@capgo/camera-preview 7.16.4 → 7.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1028,14 +1028,15 @@ Represents EXIF data extracted from an image.
1028
1028
 
1029
1029
  Defines the options for capturing a picture.
1030
1030
 
1031
- | Prop | Type | Description | Default | Since |
1032
- | ---------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ----- |
1033
- | **`height`** | <code>number</code> | The maximum height of the picture in pixels. The image will be resized to fit within this height while maintaining aspect ratio. If not specified the captured image will match the preview's visible area. | | |
1034
- | **`width`** | <code>number</code> | The maximum width of the picture in pixels. The image will be resized to fit within this width while maintaining aspect ratio. If not specified the captured image will match the preview's visible area. | | |
1035
- | **`quality`** | <code>number</code> | The quality of the captured image, from 0 to 100. Does not apply to `png` format. | <code>85</code> | |
1036
- | **`format`** | <code><a href="#pictureformat">PictureFormat</a></code> | The format of the captured image. | <code>"jpeg"</code> | |
1037
- | **`saveToGallery`** | <code>boolean</code> | If true, the captured image will be saved to the user's gallery. | <code>false</code> | 7.5.0 |
1038
- | **`withExifLocation`** | <code>boolean</code> | If true, the plugin will attempt to add GPS location data to the image's EXIF metadata. This may prompt the user for location permissions. | <code>false</code> | 7.6.0 |
1031
+ | Prop | Type | Description | Default | Since |
1032
+ | ---------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ------ |
1033
+ | **`height`** | <code>number</code> | The maximum height of the picture in pixels. The image will be resized to fit within this height while maintaining aspect ratio. If not specified the captured image will match the preview's visible area. | | |
1034
+ | **`width`** | <code>number</code> | The maximum width of the picture in pixels. The image will be resized to fit within this width while maintaining aspect ratio. If not specified the captured image will match the preview's visible area. | | |
1035
+ | **`quality`** | <code>number</code> | The quality of the captured image, from 0 to 100. Does not apply to `png` format. | <code>85</code> | |
1036
+ | **`format`** | <code><a href="#pictureformat">PictureFormat</a></code> | The format of the captured image. | <code>"jpeg"</code> | |
1037
+ | **`saveToGallery`** | <code>boolean</code> | If true, the captured image will be saved to the user's gallery. | <code>false</code> | 7.5.0 |
1038
+ | **`withExifLocation`** | <code>boolean</code> | If true, the plugin will attempt to add GPS location data to the image's EXIF metadata. This may prompt the user for location permissions. | <code>false</code> | 7.6.0 |
1039
+ | **`embedTimestamp`** | <code>boolean</code> | If true, the plugin will embed a timestamp in the top-right corner of the image. | <code>false</code> | 7.17.0 |
1039
1040
 
1040
1041
 
1041
1042
  #### CameraSampleOptions
@@ -369,8 +369,18 @@ public class CameraPreview
369
369
  );
370
370
  Integer width = call.getInt("width");
371
371
  Integer height = call.getInt("height");
372
+ final boolean embedTimestamp = Boolean.TRUE.equals(
373
+ call.getBoolean("embedTimestamp")
374
+ );
372
375
 
373
- cameraXView.capturePhoto(quality, saveToGallery, width, height, location);
376
+ cameraXView.capturePhoto(
377
+ quality,
378
+ saveToGallery,
379
+ width,
380
+ height,
381
+ location,
382
+ embedTimestamp
383
+ );
374
384
  }
375
385
 
376
386
  @PluginMethod
@@ -6,9 +6,12 @@ import android.content.pm.PackageManager;
6
6
  import android.content.res.Configuration;
7
7
  import android.graphics.Bitmap;
8
8
  import android.graphics.BitmapFactory;
9
+ import android.graphics.Canvas;
9
10
  import android.graphics.Color;
10
11
  import android.graphics.Matrix;
12
+ import android.graphics.Paint;
11
13
  import android.graphics.Rect;
14
+ import android.graphics.Typeface;
12
15
  import android.graphics.drawable.GradientDrawable;
13
16
  import android.hardware.camera2.CameraAccessException;
14
17
  import android.hardware.camera2.CameraCharacteristics;
@@ -86,6 +89,7 @@ import java.text.SimpleDateFormat;
86
89
  import java.util.ArrayList;
87
90
  import java.util.Arrays;
88
91
  import java.util.Collections;
92
+ import java.util.Date;
89
93
  import java.util.List;
90
94
  import java.util.Locale;
91
95
  import java.util.Objects;
@@ -1164,7 +1168,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1164
1168
  final boolean saveToGallery,
1165
1169
  Integer width,
1166
1170
  Integer height,
1167
- Location location
1171
+ Location location,
1172
+ final boolean embedTimestamp
1168
1173
  ) {
1169
1174
  // Prevent capture if a stop is pending
1170
1175
  if (IsOperationRunning("capturePhoto")) {
@@ -1173,12 +1178,16 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1173
1178
  }
1174
1179
  Log.d(
1175
1180
  TAG,
1176
- "capturePhoto: Starting photo capture with quality: " +
1181
+ "capturePhoto: Starting photo capture with: " +
1177
1182
  quality +
1178
1183
  ", width: " +
1179
1184
  width +
1180
1185
  ", height: " +
1181
- height
1186
+ height +
1187
+ ", saveToGallery: " +
1188
+ saveToGallery +
1189
+ ", embedTimestamp: " +
1190
+ embedTimestamp
1182
1191
  );
1183
1192
 
1184
1193
  if (imageCapture == null) {
@@ -1253,6 +1262,12 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1253
1262
  width,
1254
1263
  height
1255
1264
  );
1265
+ if (embedTimestamp) {
1266
+ resizedBitmap = drawTimestampOntoBitmap(
1267
+ resizedBitmap,
1268
+ exifInterface
1269
+ );
1270
+ }
1256
1271
  ByteArrayOutputStream stream = new ByteArrayOutputStream();
1257
1272
  resizedBitmap.compress(
1258
1273
  Bitmap.CompressFormat.JPEG,
@@ -1287,6 +1302,12 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1287
1302
  exifInterface
1288
1303
  );
1289
1304
  Bitmap previewCropped = cropBitmapToMatchPreview(originalBitmap);
1305
+ if (embedTimestamp) {
1306
+ previewCropped = drawTimestampOntoBitmap(
1307
+ previewCropped,
1308
+ exifInterface
1309
+ );
1310
+ }
1290
1311
  ByteArrayOutputStream stream = new ByteArrayOutputStream();
1291
1312
  previewCropped.compress(
1292
1313
  Bitmap.CompressFormat.JPEG,
@@ -1385,6 +1406,108 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1385
1406
  );
1386
1407
  }
1387
1408
 
1409
+ private Bitmap drawTimestampOntoBitmap(Bitmap src, ExifInterface exif) {
1410
+ if (src == null) return null;
1411
+
1412
+ // Build timestamp string ("yyyy-MM-dd HH:mm:ss"), preferring EXIF original
1413
+ final String fmt = "yyyy-MM-dd HH:mm:ss";
1414
+ String when = null;
1415
+ if (exif != null) {
1416
+ final String exifDate = exif.getAttribute(
1417
+ ExifInterface.TAG_DATETIME_ORIGINAL
1418
+ ); // "yyyy:MM:dd HH:mm:ss"
1419
+ if (exifDate != null && !exifDate.trim().isEmpty()) {
1420
+ try {
1421
+ java.text.SimpleDateFormat inFmt = new java.text.SimpleDateFormat(
1422
+ "yyyy:MM:dd HH:mm:ss",
1423
+ java.util.Locale.US
1424
+ );
1425
+ java.util.Date d = inFmt.parse(exifDate);
1426
+ if (d != null) {
1427
+ when = new java.text.SimpleDateFormat(
1428
+ fmt,
1429
+ java.util.Locale.getDefault()
1430
+ ).format(d);
1431
+ }
1432
+ } catch (java.text.ParseException ignored) {}
1433
+ }
1434
+ }
1435
+ if (when == null) {
1436
+ when = new java.text.SimpleDateFormat(
1437
+ fmt,
1438
+ java.util.Locale.getDefault()
1439
+ ).format(new java.util.Date());
1440
+ }
1441
+
1442
+ final Bitmap bmp = src.isMutable()
1443
+ ? src
1444
+ : src.copy(Bitmap.Config.ARGB_8888, true);
1445
+ final Canvas canvas = new Canvas(bmp);
1446
+
1447
+ // ---- Match iOS constants ----
1448
+ final float fontPx = Math.max(10f, bmp.getWidth() * 0.035f); // ≥10, 3.5% of width
1449
+ final float paddingH = 16f;
1450
+ final float paddingV = 10f;
1451
+ final float cornerRadius = 10f;
1452
+ final float margin = 12f;
1453
+
1454
+ // Text paint (SF .semibold ≈ Roboto Medium)
1455
+ final Paint text = new Paint(
1456
+ Paint.ANTI_ALIAS_FLAG | Paint.SUBPIXEL_TEXT_FLAG | Paint.LINEAR_TEXT_FLAG
1457
+ );
1458
+ text.setColor(Color.WHITE);
1459
+ text.setTypeface(Typeface.create("sans-serif-medium", Typeface.NORMAL));
1460
+ text.setTextSize(fontPx);
1461
+ text.setTextAlign(Paint.Align.LEFT);
1462
+ text.setDither(true);
1463
+
1464
+ // Measure text
1465
+ final float textWidth = text.measureText(when);
1466
+ final Paint.FontMetrics fm = text.getFontMetrics();
1467
+ final float textHeight = fm.descent - fm.ascent;
1468
+
1469
+ // Bubble rect (top-right)
1470
+ final float bgWidth = textWidth + paddingH * 2f;
1471
+ final float bgHeight = textHeight + paddingV * 2f;
1472
+ final float bgLeft = Math.max(0, bmp.getWidth() - bgWidth - margin);
1473
+ final float bgTop = margin;
1474
+ final float bgRight = bgLeft + bgWidth;
1475
+ final float bgBottom = bgTop + bgHeight;
1476
+
1477
+ // Background color: UIColor(white:0.12, alpha:0.22)
1478
+ // -> alpha ≈ 0.22*255 ≈ 56, rgb ≈ 0.12*255 ≈ 31
1479
+ final int bgColor = Color.argb(56, 31, 31, 31);
1480
+
1481
+ final Paint bg = new Paint(Paint.ANTI_ALIAS_FLAG);
1482
+ bg.setColor(bgColor);
1483
+ bg.setStyle(Paint.Style.FILL);
1484
+ // Shadow: offset (0,2), blur ~6, alpha 0.25 black
1485
+ bg.setShadowLayer(6f, 0f, 2f, Color.argb(64, 0, 0, 0));
1486
+
1487
+ // Draw bubble
1488
+ canvas.drawRoundRect(
1489
+ bgLeft,
1490
+ bgTop,
1491
+ bgRight,
1492
+ bgBottom,
1493
+ cornerRadius,
1494
+ cornerRadius,
1495
+ bg
1496
+ );
1497
+
1498
+ // Text origin (like iOS: top-left inside padding)
1499
+ final float textX = bgLeft + paddingH;
1500
+ final float textY = bgTop + paddingV - fm.ascent; // convert top-left to baseline
1501
+
1502
+ // High-quality rendering akin to iOS flags
1503
+ text.setFilterBitmap(true);
1504
+ text.setHinting(Paint.HINTING_ON);
1505
+
1506
+ canvas.drawText(when, textX, textY, text);
1507
+
1508
+ return bmp;
1509
+ }
1510
+
1388
1511
  private int exifToDegrees(int exifOrientation) {
1389
1512
  switch (exifOrientation) {
1390
1513
  case ExifInterface.ORIENTATION_ROTATE_90:
package/dist/docs.json CHANGED
@@ -1428,6 +1428,22 @@
1428
1428
  "docs": "If true, the plugin will attempt to add GPS location data to the image's EXIF metadata.\nThis may prompt the user for location permissions.",
1429
1429
  "complexTypes": [],
1430
1430
  "type": "boolean | undefined"
1431
+ },
1432
+ {
1433
+ "name": "embedTimestamp",
1434
+ "tags": [
1435
+ {
1436
+ "text": "false",
1437
+ "name": "default"
1438
+ },
1439
+ {
1440
+ "text": "7.17.0",
1441
+ "name": "since"
1442
+ }
1443
+ ],
1444
+ "docs": "If true, the plugin will embed a timestamp in the top-right corner of the image.",
1445
+ "complexTypes": [],
1446
+ "type": "boolean | undefined"
1431
1447
  }
1432
1448
  ]
1433
1449
  },
@@ -238,6 +238,12 @@ export interface CameraPreviewPictureOptions {
238
238
  * @since 7.6.0
239
239
  */
240
240
  withExifLocation?: boolean;
241
+ /**
242
+ * If true, the plugin will embed a timestamp in the top-right corner of the image.
243
+ * @default false
244
+ * @since 7.17.0
245
+ */
246
+ embedTimestamp?: boolean;
241
247
  }
242
248
  /** Represents EXIF data extracted from an image. */
243
249
  export interface ExifData {
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"AAUA,MAAM,CAAN,IAAY,UAQX;AARD,WAAY,UAAU;IACpB,sCAAwB,CAAA;IACxB,sCAAwB,CAAA;IACxB,qCAAuB,CAAA;IACvB,sCAAwB,CAAA;IACxB,2BAAa,CAAA;IACb,oCAAsB,CAAA;IACtB,+BAAiB,CAAA;AACnB,CAAC,EARW,UAAU,KAAV,UAAU,QAQrB","sourcesContent":["import type { PluginListenerHandle } from \"@capacitor/core\";\n\nexport type CameraPosition = \"rear\" | \"front\";\n\nexport type FlashMode = CameraPreviewFlashMode;\n\nexport type GridMode = \"none\" | \"3x3\" | \"4x4\";\n\nexport type CameraPositioning = \"center\" | \"top\" | \"bottom\";\n\nexport enum DeviceType {\n ULTRA_WIDE = \"ultraWide\",\n WIDE_ANGLE = \"wideAngle\",\n TELEPHOTO = \"telephoto\",\n TRUE_DEPTH = \"trueDepth\",\n DUAL = \"dual\",\n DUAL_WIDE = \"dualWide\",\n TRIPLE = \"triple\",\n}\n\n/**\n * Represents a single camera lens on a device. A {@link CameraDevice} can have multiple lenses.\n */\nexport interface CameraLens {\n /** A human-readable name for the lens, e.g., \"Ultra-Wide\". */\n label: string;\n /** The type of the camera lens. */\n deviceType: DeviceType;\n /** The focal length of the lens in millimeters. */\n focalLength: number;\n /** The base zoom factor for this lens (e.g., 0.5 for ultra-wide, 1.0 for wide). */\n baseZoomRatio: number;\n /** The minimum zoom factor supported by this specific lens. */\n minZoom: number;\n /** The maximum zoom factor supported by this specific lens. */\n maxZoom: number;\n}\n\n/**\n * Represents a physical camera on the device (e.g., the front-facing camera).\n */\nexport interface CameraDevice {\n /** A unique identifier for the camera device. */\n deviceId: string;\n /** A human-readable name for the camera device. */\n label: string;\n /** The physical position of the camera on the device. */\n position: CameraPosition;\n /** A list of all available lenses for this camera device. */\n lenses: CameraLens[];\n /** The overall minimum zoom factor available across all lenses on this device. */\n minZoom: number;\n /** The overall maximum zoom factor available across all lenses on this device. */\n maxZoom: number;\n /** Identifies whether the device is a logical camera (composed of multiple physical lenses). */\n isLogical: boolean;\n}\n\n/**\n * Represents the detailed information of the currently active lens.\n */\nexport interface LensInfo {\n /** The focal length of the active lens in millimeters. */\n focalLength: number;\n /** The device type of the active lens. */\n deviceType: DeviceType;\n /** The base zoom ratio of the active lens (e.g., 0.5x, 1.0x). */\n baseZoomRatio: number;\n /** The current digital zoom factor applied on top of the base zoom. */\n digitalZoom: number;\n}\n\n/**\n * Defines the configuration options for starting the camera preview.\n */\nexport interface CameraPreviewOptions {\n /**\n * The parent element to attach the video preview to.\n * @platform web\n */\n parent?: string;\n /**\n * A CSS class name to add to the preview element.\n * @platform web\n */\n className?: string;\n /**\n * The width of the preview in pixels. Defaults to the screen width.\n * @platform android, ios, web\n */\n width?: number;\n /**\n * The height of the preview in pixels. Defaults to the screen height.\n * @platform android, ios, web\n */\n height?: number;\n /**\n * The horizontal origin of the preview, in pixels.\n * @platform android, ios\n */\n x?: number;\n /**\n * The vertical origin of the preview, in pixels.\n * @platform android, ios\n */\n y?: number;\n /**\n * The aspect ratio of the camera preview, '4:3' or '16:9' or 'fill'.\n * Cannot be set if width or height is provided, otherwise the call will be rejected.\n * Use setPreviewSize to adjust size after starting.\n *\n * @since 2.0.0\n */\n aspectRatio?: \"4:3\" | \"16:9\";\n /**\n * The grid overlay to display on the camera preview.\n * @default \"none\"\n * @since 2.1.0\n */\n gridMode?: GridMode;\n /**\n * Adjusts the y-position to account for safe areas (e.g., notches).\n * @platform ios\n * @default false\n */\n includeSafeAreaInsets?: boolean;\n /**\n * If true, places the preview behind the webview.\n * @platform android\n * @default true\n */\n toBack?: boolean;\n /**\n * Bottom padding for the preview, in pixels.\n * @platform android, ios\n */\n paddingBottom?: number;\n /**\n * Whether to rotate the preview when the device orientation changes.\n * @platform ios\n * @default true\n */\n rotateWhenOrientationChanged?: boolean;\n /**\n * The camera to use.\n * @default \"rear\"\n */\n position?: CameraPosition | string;\n /**\n * If true, saves the captured image to a file and returns the file path.\n * If false, returns a base64 encoded string.\n * @default false\n */\n storeToFile?: boolean;\n /**\n * If true, prevents the plugin from rotating the image based on EXIF data.\n * @platform android\n * @default false\n */\n disableExifHeaderStripping?: boolean;\n /**\n * If true, disables the audio stream, preventing audio permission requests.\n * @default true\n */\n disableAudio?: boolean;\n /**\n * If true, locks the device orientation while the camera is active.\n * @platform android\n * @default false\n */\n lockAndroidOrientation?: boolean;\n /**\n * If true, allows the camera preview's opacity to be changed.\n * @platform android, web\n * @default false\n */\n enableOpacity?: boolean;\n\n /**\n * If true, disables the visual focus indicator when tapping to focus.\n * @platform android, ios\n * @default false\n */\n disableFocusIndicator?: boolean;\n /**\n * The `deviceId` of the camera to use. If provided, `position` is ignored.\n * @platform ios\n */\n deviceId?: string;\n /**\n * The initial zoom level when starting the camera preview.\n * If the requested zoom level is not available, the native plugin will reject.\n * @default 1.0\n * @platform android, ios\n * @since 2.2.0\n */\n initialZoomLevel?: number;\n /**\n * The vertical positioning of the camera preview.\n * @default \"center\"\n * @platform android, ios, web\n * @since 2.3.0\n */\n positioning?: CameraPositioning;\n /**\n * If true, enables video capture capabilities when the camera starts.\n * @default false\n * @platform android\n * @since 7.11.0\n */\n enableVideoMode?: boolean;\n}\n\n/**\n * Defines the options for capturing a picture.\n */\nexport interface CameraPreviewPictureOptions {\n /**\n * The maximum height of the picture in pixels. The image will be resized to fit within this height while maintaining aspect ratio.\n * If not specified the captured image will match the preview's visible area.\n */\n height?: number;\n /**\n * The maximum width of the picture in pixels. The image will be resized to fit within this width while maintaining aspect ratio.\n * If not specified the captured image will match the preview's visible area.\n */\n width?: number;\n /**\n * The quality of the captured image, from 0 to 100.\n * Does not apply to `png` format.\n * @default 85\n */\n quality?: number;\n /**\n * The format of the captured image.\n * @default \"jpeg\"\n */\n format?: PictureFormat;\n /**\n * If true, the captured image will be saved to the user's gallery.\n * @default false\n * @since 7.5.0\n */\n saveToGallery?: boolean;\n /**\n * If true, the plugin will attempt to add GPS location data to the image's EXIF metadata.\n * This may prompt the user for location permissions.\n * @default false\n * @since 7.6.0\n */\n withExifLocation?: boolean;\n}\n\n/** Represents EXIF data extracted from an image. */\nexport interface ExifData {\n [key: string]: any;\n}\n\nexport type PictureFormat = \"jpeg\" | \"png\";\n\n/** Defines a standard picture size with width and height. */\nexport interface PictureSize {\n /** The width of the picture in pixels. */\n width: number;\n /** The height of the picture in pixels. */\n height: number;\n}\n\n/** Represents the supported picture sizes for a camera facing a certain direction. */\nexport interface SupportedPictureSizes {\n /** The camera direction (\"front\" or \"rear\"). */\n facing: string;\n /** A list of supported picture sizes for this camera. */\n supportedPictureSizes: PictureSize[];\n}\n\n/**\n * Defines the options for capturing a sample frame from the camera preview.\n */\nexport interface CameraSampleOptions {\n /**\n * The quality of the captured sample, from 0 to 100.\n * @default 85\n */\n quality?: number;\n}\n\n/**\n * The available flash modes for the camera.\n * 'torch' is a continuous light mode.\n */\nexport type CameraPreviewFlashMode = \"off\" | \"on\" | \"auto\" | \"torch\";\n\n/** Reusable exposure mode type for cross-platform support. */\nexport type ExposureMode = \"AUTO\" | \"LOCK\" | \"CONTINUOUS\" | \"CUSTOM\";\n\n/**\n * Defines the options for setting the camera preview's opacity.\n */\nexport interface CameraOpacityOptions {\n /**\n * The opacity percentage, from 0.0 (fully transparent) to 1.0 (fully opaque).\n * @default 1.0\n */\n opacity?: number;\n}\n\n/**\n * Represents safe area insets for devices.\n * Android: Values are expressed in logical pixels (dp) to match JS layout units.\n * iOS: Values are expressed in physical pixels and exclude status bar.\n */\nexport interface SafeAreaInsets {\n /** Current device orientation (1 = portrait, 2 = landscape, 0 = unknown). */\n orientation: number;\n /**\n * Orientation-aware notch/camera cutout inset (excluding status bar).\n * In portrait mode: returns top inset (notch at top).\n * In landscape mode: returns left inset (notch at side).\n * Android: Value in dp, iOS: Value in pixels (status bar excluded).\n */\n top: number;\n}\n\n/**\n * Canonical device orientation values across platforms.\n */\nexport type DeviceOrientation =\n | \"portrait\"\n | \"landscape-left\"\n | \"landscape-right\"\n | \"portrait-upside-down\"\n | \"unknown\";\n\n/**\n * The main interface for the CameraPreview plugin.\n */\nexport interface CameraPreviewPlugin {\n /**\n * Starts the camera preview.\n *\n * @param {CameraPreviewOptions} options - The configuration for the camera preview.\n * @returns {Promise<{ width: number; height: number; x: number; y: number }>} A promise that resolves with the preview dimensions.\n * @since 0.0.1\n */\n start(options: CameraPreviewOptions): Promise<{\n /** The width of the preview in pixels. */\n width: number;\n /** The height of the preview in pixels. */\n height: number;\n /** The horizontal origin of the preview, in pixels. */\n x: number;\n /** The vertical origin of the preview, in pixels. */\n y: number;\n }>;\n\n /**\n * Stops the camera preview.\n *\n * @returns {Promise<void>} A promise that resolves when the camera preview is stopped.\n * @since 0.0.1\n */\n stop(): Promise<void>;\n\n /**\n * Captures a picture from the camera.\n *\n * If `storeToFile` was set to `true` when starting the preview, the returned\n * `value` will be an absolute file path on the device instead of a base64 string. Use getBase64FromFilePath to get the base64 string from the file path.\n *\n * @param {CameraPreviewPictureOptions} options - The options for capturing the picture.\n * @returns {Promise<{ value: string; exif: ExifData }>} Resolves with:\n * - `value`: base64 string, or file path if `storeToFile` is true\n * - `exif`: extracted EXIF metadata when available\n * @since 0.0.1\n */\n capture(\n options: CameraPreviewPictureOptions,\n ): Promise<{ value: string; exif: ExifData }>;\n\n /**\n * Captures a single frame from the camera preview stream.\n *\n * @param {CameraSampleOptions} options - The options for capturing the sample.\n * @returns {Promise<{ value: string }>} A promise that resolves with the sample image as a base64 encoded string.\n * @since 0.0.1\n */\n captureSample(options: CameraSampleOptions): Promise<{ value: string }>;\n\n /**\n * Gets the flash modes supported by the active camera.\n *\n * @returns {Promise<{ result: CameraPreviewFlashMode[] }>} A promise that resolves with an array of supported flash modes.\n * @since 0.0.1\n */\n getSupportedFlashModes(): Promise<{\n result: CameraPreviewFlashMode[];\n }>;\n\n /**\n * Set the aspect ratio of the camera preview.\n *\n * @param {{ aspectRatio: '4:3' | '16:9'; x?: number; y?: number }} options - The desired aspect ratio and optional position.\n * - aspectRatio: The desired aspect ratio ('4:3' or '16:9')\n * - x: Optional x coordinate for positioning. If not provided, view will be auto-centered horizontally.\n * - y: Optional y coordinate for positioning. If not provided, view will be auto-centered vertically.\n * @returns {Promise<{ width: number; height: number; x: number; y: number }>} A promise that resolves with the actual preview dimensions and position.\n * @since 7.5.0\n * @platform android, ios\n */\n setAspectRatio(options: {\n aspectRatio: \"4:3\" | \"16:9\";\n x?: number;\n y?: number;\n }): Promise<{\n width: number;\n height: number;\n x: number;\n y: number;\n }>;\n\n /**\n * Gets the current aspect ratio of the camera preview.\n *\n * @returns {Promise<{ aspectRatio: '4:3' | '16:9' }>} A promise that resolves with the current aspect ratio.\n * @since 7.5.0\n * @platform android, ios\n */\n getAspectRatio(): Promise<{ aspectRatio: \"4:3\" | \"16:9\" }>;\n\n /**\n * Sets the grid mode of the camera preview overlay.\n *\n * @param {{ gridMode: GridMode }} options - The desired grid mode ('none', '3x3', or '4x4').\n * @returns {Promise<void>} A promise that resolves when the grid mode is set.\n * @since 8.0.0\n */\n setGridMode(options: { gridMode: GridMode }): Promise<void>;\n\n /**\n * Gets the current grid mode of the camera preview overlay.\n *\n * @returns {Promise<{ gridMode: GridMode }>} A promise that resolves with the current grid mode.\n * @since 8.0.0\n */\n getGridMode(): Promise<{ gridMode: GridMode }>;\n\n /**\n * Gets the horizontal field of view (FoV) for the active camera.\n * Note: This can be an estimate on some devices.\n *\n * @returns {Promise<{ result: number }>} A promise that resolves with the horizontal field of view in degrees.\n * @since 0.0.1\n */\n getHorizontalFov(): Promise<{\n result: number;\n }>;\n\n /**\n * Gets the supported picture sizes for all cameras.\n *\n * @returns {Promise<{ supportedPictureSizes: SupportedPictureSizes[] }>} A promise that resolves with the list of supported sizes.\n * @since 7.4.0\n */\n getSupportedPictureSizes(): Promise<{\n supportedPictureSizes: SupportedPictureSizes[];\n }>;\n\n /**\n * Sets the flash mode for the active camera.\n *\n * @param {{ flashMode: CameraPreviewFlashMode | string }} options - The desired flash mode.\n * @returns {Promise<void>} A promise that resolves when the flash mode is set.\n * @since 0.0.1\n */\n setFlashMode(options: {\n flashMode: CameraPreviewFlashMode | string;\n }): Promise<void>;\n\n /**\n * Toggles between the front and rear cameras.\n *\n * @returns {Promise<void>} A promise that resolves when the camera is flipped.\n * @since 0.0.1\n */\n flip(): Promise<void>;\n\n /**\n * Sets the opacity of the camera preview.\n *\n * @param {CameraOpacityOptions} options - The opacity options.\n * @returns {Promise<void>} A promise that resolves when the opacity is set.\n * @since 0.0.1\n */\n setOpacity(options: CameraOpacityOptions): Promise<void>;\n\n /**\n * Stops an ongoing video recording.\n *\n * @returns {Promise<{ videoFilePath: string }>} A promise that resolves with the path to the recorded video file.\n * @since 0.0.1\n */\n stopRecordVideo(): Promise<{ videoFilePath: string }>;\n\n /**\n * Starts recording a video.\n *\n * @param {CameraPreviewOptions} options - The options for video recording. Only iOS.\n * @returns {Promise<void>} A promise that resolves when video recording starts.\n * @since 0.0.1\n */\n startRecordVideo(options: CameraPreviewOptions): Promise<void>;\n\n /**\n * Checks if the camera preview is currently running.\n *\n * @returns {Promise<{ isRunning: boolean }>} A promise that resolves with the running state.\n * @since 7.5.0\n * @platform android, ios\n */\n isRunning(): Promise<{ isRunning: boolean }>;\n\n /**\n * Gets all available camera devices.\n *\n * @returns {Promise<{ devices: CameraDevice[] }>} A promise that resolves with the list of available camera devices.\n * @since 7.5.0\n * @platform android, ios\n */\n getAvailableDevices(): Promise<{ devices: CameraDevice[] }>;\n\n /**\n * Gets the current zoom state, including min/max and current lens info.\n *\n * @returns {Promise<{ min: number; max: number; current: number; lens: LensInfo }>} A promise that resolves with the zoom state.\n * @since 7.5.0\n * @platform android, ios\n */\n getZoom(): Promise<{\n min: number;\n max: number;\n current: number;\n lens: LensInfo;\n }>;\n\n /**\n * Returns zoom button values for quick switching.\n * - iOS/Android: includes 0.5 if ultra-wide available; 1 and 2 if wide available; 3 if telephoto available\n * - Web: unsupported\n * @since 7.5.0\n * @platform android, ios\n */\n getZoomButtonValues(): Promise<{ values: number[] }>;\n\n /**\n * Sets the zoom level of the camera.\n *\n * @param {{ level: number; ramp?: boolean; autoFocus?: boolean }} options - The desired zoom level. `ramp` is currently unused. `autoFocus` defaults to true.\n * @returns {Promise<void>} A promise that resolves when the zoom level is set.\n * @since 7.5.0\n * @platform android, ios\n */\n setZoom(options: {\n level: number;\n ramp?: boolean;\n autoFocus?: boolean;\n }): Promise<void>;\n\n /**\n * Gets the current flash mode.\n *\n * @returns {Promise<{ flashMode: FlashMode }>} A promise that resolves with the current flash mode.\n * @since 7.5.0\n * @platform android, ios\n */\n getFlashMode(): Promise<{ flashMode: FlashMode }>;\n\n /**\n * Removes all registered listeners.\n *\n * @since 7.5.0\n * @platform android, ios\n */\n removeAllListeners(): Promise<void>;\n\n /**\n * Switches the active camera to the one with the specified `deviceId`.\n *\n * @param {{ deviceId: string }} options - The ID of the device to switch to.\n * @returns {Promise<void>} A promise that resolves when the camera is switched.\n * @since 7.5.0\n * @platform android, ios\n */\n setDeviceId(options: { deviceId: string }): Promise<void>;\n\n /**\n * Gets the ID of the currently active camera device.\n *\n * @returns {Promise<{ deviceId: string }>} A promise that resolves with the current device ID.\n * @since 7.5.0\n * @platform android, ios\n */\n getDeviceId(): Promise<{ deviceId: string }>;\n\n /**\n * Gets the current preview size and position.\n * @returns {Promise<{x: number, y: number, width: number, height: number}>}\n * @since 7.5.0\n * @platform android, ios\n */\n getPreviewSize(): Promise<{\n x: number;\n y: number;\n width: number;\n height: number;\n }>;\n /**\n * Sets the preview size and position.\n * @param options The new position and dimensions.\n * @returns {Promise<{ width: number; height: number; x: number; y: number }>} A promise that resolves with the actual preview dimensions and position.\n * @since 7.5.0\n * @platform android, ios\n */\n setPreviewSize(options: {\n x?: number;\n y?: number;\n width: number;\n height: number;\n }): Promise<{\n width: number;\n height: number;\n x: number;\n y: number;\n }>;\n\n /**\n * Sets the camera focus to a specific point in the preview.\n *\n * Note: The plugin does not attach any native tap-to-focus gesture handlers. Handle taps in\n * your HTML/JS (e.g., on the overlaying UI), then pass normalized coordinates here.\n *\n * @param {Object} options - The focus options.\n * @param {number} options.x - The x coordinate in the preview view to focus on (0-1 normalized).\n * @param {number} options.y - The y coordinate in the preview view to focus on (0-1 normalized).\n * @returns {Promise<void>} A promise that resolves when the focus is set.\n * @since 7.5.0\n * @platform android, ios\n */\n setFocus(options: { x: number; y: number }): Promise<void>;\n\n /**\n * Adds a listener for screen resize events.\n * @param {string} eventName - The event name to listen for.\n * @param {Function} listenerFunc - The function to call when the event is triggered.\n * @returns {Promise<PluginListenerHandle>} A promise that resolves with a handle to the listener.\n * @since 7.5.0\n * @platform android, ios\n */\n addListener(\n eventName: \"screenResize\",\n listenerFunc: (data: {\n width: number;\n height: number;\n x: number;\n y: number;\n }) => void,\n ): Promise<PluginListenerHandle>;\n\n /**\n * Adds a listener for orientation change events.\n * @param {string} eventName - The event name to listen for.\n * @param {Function} listenerFunc - The function to call when the event is triggered.\n * @returns {Promise<PluginListenerHandle>} A promise that resolves with a handle to the listener.\n * @since 7.5.0\n * @platform android, ios\n */\n addListener(\n eventName: \"orientationChange\",\n listenerFunc: (data: { orientation: DeviceOrientation }) => void,\n ): Promise<PluginListenerHandle>;\n /**\n * Deletes a file at the given absolute path on the device.\n * Use this to quickly clean up temporary images created with `storeToFile`.\n * On web, this is not supported and will throw.\n * @since 7.5.0\n * @platform android, ios\n */\n deleteFile(options: { path: string }): Promise<{ success: boolean }>;\n\n /**\n * Gets the safe area insets for devices.\n * Returns the orientation-aware notch/camera cutout inset and the current orientation.\n * In portrait mode: returns top inset (notch at top).\n * In landscape mode: returns left inset (notch moved to side).\n * This specifically targets the cutout area (notch, punch hole, etc.) that all modern phones have.\n *\n * Android: Values returned in dp (logical pixels).\n * iOS: Values returned in physical pixels, excluding status bar (only pure notch/cutout size).\n *\n * @platform android, ios\n */\n getSafeAreaInsets(): Promise<SafeAreaInsets>;\n\n /**\n * Gets the current device orientation in a cross-platform format.\n * @since 7.5.0\n * @platform android, ios\n */\n getOrientation(): Promise<{ orientation: DeviceOrientation }>;\n\n /**\n * Returns the exposure modes supported by the active camera.\n * Modes can include: 'locked', 'auto', 'continuous', 'custom'.\n * @platform android, ios\n */\n getExposureModes(): Promise<{ modes: ExposureMode[] }>;\n\n /**\n * Returns the current exposure mode.\n * @platform android, ios\n */\n getExposureMode(): Promise<{ mode: ExposureMode }>;\n\n /**\n * Sets the exposure mode.\n * @platform android, ios\n */\n setExposureMode(options: { mode: ExposureMode }): Promise<void>;\n\n /**\n * Returns the exposure compensation (EV bias) supported range.\n * @platform ios\n */\n getExposureCompensationRange(): Promise<{\n min: number;\n max: number;\n step: number;\n }>;\n\n /**\n * Returns the current exposure compensation (EV bias).\n * @platform ios\n */\n getExposureCompensation(): Promise<{ value: number }>;\n\n /**\n * Sets the exposure compensation (EV bias). Value will be clamped to range.\n * @platform ios\n */\n setExposureCompensation(options: { value: number }): Promise<void>;\n}\n"]}
1
+ {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"AAUA,MAAM,CAAN,IAAY,UAQX;AARD,WAAY,UAAU;IACpB,sCAAwB,CAAA;IACxB,sCAAwB,CAAA;IACxB,qCAAuB,CAAA;IACvB,sCAAwB,CAAA;IACxB,2BAAa,CAAA;IACb,oCAAsB,CAAA;IACtB,+BAAiB,CAAA;AACnB,CAAC,EARW,UAAU,KAAV,UAAU,QAQrB","sourcesContent":["import type { PluginListenerHandle } from \"@capacitor/core\";\n\nexport type CameraPosition = \"rear\" | \"front\";\n\nexport type FlashMode = CameraPreviewFlashMode;\n\nexport type GridMode = \"none\" | \"3x3\" | \"4x4\";\n\nexport type CameraPositioning = \"center\" | \"top\" | \"bottom\";\n\nexport enum DeviceType {\n ULTRA_WIDE = \"ultraWide\",\n WIDE_ANGLE = \"wideAngle\",\n TELEPHOTO = \"telephoto\",\n TRUE_DEPTH = \"trueDepth\",\n DUAL = \"dual\",\n DUAL_WIDE = \"dualWide\",\n TRIPLE = \"triple\",\n}\n\n/**\n * Represents a single camera lens on a device. A {@link CameraDevice} can have multiple lenses.\n */\nexport interface CameraLens {\n /** A human-readable name for the lens, e.g., \"Ultra-Wide\". */\n label: string;\n /** The type of the camera lens. */\n deviceType: DeviceType;\n /** The focal length of the lens in millimeters. */\n focalLength: number;\n /** The base zoom factor for this lens (e.g., 0.5 for ultra-wide, 1.0 for wide). */\n baseZoomRatio: number;\n /** The minimum zoom factor supported by this specific lens. */\n minZoom: number;\n /** The maximum zoom factor supported by this specific lens. */\n maxZoom: number;\n}\n\n/**\n * Represents a physical camera on the device (e.g., the front-facing camera).\n */\nexport interface CameraDevice {\n /** A unique identifier for the camera device. */\n deviceId: string;\n /** A human-readable name for the camera device. */\n label: string;\n /** The physical position of the camera on the device. */\n position: CameraPosition;\n /** A list of all available lenses for this camera device. */\n lenses: CameraLens[];\n /** The overall minimum zoom factor available across all lenses on this device. */\n minZoom: number;\n /** The overall maximum zoom factor available across all lenses on this device. */\n maxZoom: number;\n /** Identifies whether the device is a logical camera (composed of multiple physical lenses). */\n isLogical: boolean;\n}\n\n/**\n * Represents the detailed information of the currently active lens.\n */\nexport interface LensInfo {\n /** The focal length of the active lens in millimeters. */\n focalLength: number;\n /** The device type of the active lens. */\n deviceType: DeviceType;\n /** The base zoom ratio of the active lens (e.g., 0.5x, 1.0x). */\n baseZoomRatio: number;\n /** The current digital zoom factor applied on top of the base zoom. */\n digitalZoom: number;\n}\n\n/**\n * Defines the configuration options for starting the camera preview.\n */\nexport interface CameraPreviewOptions {\n /**\n * The parent element to attach the video preview to.\n * @platform web\n */\n parent?: string;\n /**\n * A CSS class name to add to the preview element.\n * @platform web\n */\n className?: string;\n /**\n * The width of the preview in pixels. Defaults to the screen width.\n * @platform android, ios, web\n */\n width?: number;\n /**\n * The height of the preview in pixels. Defaults to the screen height.\n * @platform android, ios, web\n */\n height?: number;\n /**\n * The horizontal origin of the preview, in pixels.\n * @platform android, ios\n */\n x?: number;\n /**\n * The vertical origin of the preview, in pixels.\n * @platform android, ios\n */\n y?: number;\n /**\n * The aspect ratio of the camera preview, '4:3' or '16:9' or 'fill'.\n * Cannot be set if width or height is provided, otherwise the call will be rejected.\n * Use setPreviewSize to adjust size after starting.\n *\n * @since 2.0.0\n */\n aspectRatio?: \"4:3\" | \"16:9\";\n /**\n * The grid overlay to display on the camera preview.\n * @default \"none\"\n * @since 2.1.0\n */\n gridMode?: GridMode;\n /**\n * Adjusts the y-position to account for safe areas (e.g., notches).\n * @platform ios\n * @default false\n */\n includeSafeAreaInsets?: boolean;\n /**\n * If true, places the preview behind the webview.\n * @platform android\n * @default true\n */\n toBack?: boolean;\n /**\n * Bottom padding for the preview, in pixels.\n * @platform android, ios\n */\n paddingBottom?: number;\n /**\n * Whether to rotate the preview when the device orientation changes.\n * @platform ios\n * @default true\n */\n rotateWhenOrientationChanged?: boolean;\n /**\n * The camera to use.\n * @default \"rear\"\n */\n position?: CameraPosition | string;\n /**\n * If true, saves the captured image to a file and returns the file path.\n * If false, returns a base64 encoded string.\n * @default false\n */\n storeToFile?: boolean;\n /**\n * If true, prevents the plugin from rotating the image based on EXIF data.\n * @platform android\n * @default false\n */\n disableExifHeaderStripping?: boolean;\n /**\n * If true, disables the audio stream, preventing audio permission requests.\n * @default true\n */\n disableAudio?: boolean;\n /**\n * If true, locks the device orientation while the camera is active.\n * @platform android\n * @default false\n */\n lockAndroidOrientation?: boolean;\n /**\n * If true, allows the camera preview's opacity to be changed.\n * @platform android, web\n * @default false\n */\n enableOpacity?: boolean;\n\n /**\n * If true, disables the visual focus indicator when tapping to focus.\n * @platform android, ios\n * @default false\n */\n disableFocusIndicator?: boolean;\n /**\n * The `deviceId` of the camera to use. If provided, `position` is ignored.\n * @platform ios\n */\n deviceId?: string;\n /**\n * The initial zoom level when starting the camera preview.\n * If the requested zoom level is not available, the native plugin will reject.\n * @default 1.0\n * @platform android, ios\n * @since 2.2.0\n */\n initialZoomLevel?: number;\n /**\n * The vertical positioning of the camera preview.\n * @default \"center\"\n * @platform android, ios, web\n * @since 2.3.0\n */\n positioning?: CameraPositioning;\n /**\n * If true, enables video capture capabilities when the camera starts.\n * @default false\n * @platform android\n * @since 7.11.0\n */\n enableVideoMode?: boolean;\n}\n\n/**\n * Defines the options for capturing a picture.\n */\nexport interface CameraPreviewPictureOptions {\n /**\n * The maximum height of the picture in pixels. The image will be resized to fit within this height while maintaining aspect ratio.\n * If not specified the captured image will match the preview's visible area.\n */\n height?: number;\n /**\n * The maximum width of the picture in pixels. The image will be resized to fit within this width while maintaining aspect ratio.\n * If not specified the captured image will match the preview's visible area.\n */\n width?: number;\n /**\n * The quality of the captured image, from 0 to 100.\n * Does not apply to `png` format.\n * @default 85\n */\n quality?: number;\n /**\n * The format of the captured image.\n * @default \"jpeg\"\n */\n format?: PictureFormat;\n /**\n * If true, the captured image will be saved to the user's gallery.\n * @default false\n * @since 7.5.0\n */\n saveToGallery?: boolean;\n /**\n * If true, the plugin will attempt to add GPS location data to the image's EXIF metadata.\n * This may prompt the user for location permissions.\n * @default false\n * @since 7.6.0\n */\n withExifLocation?: boolean;\n /**\n * If true, the plugin will embed a timestamp in the top-right corner of the image.\n * @default false\n * @since 7.17.0\n */\n embedTimestamp?: boolean;\n}\n\n/** Represents EXIF data extracted from an image. */\nexport interface ExifData {\n [key: string]: any;\n}\n\nexport type PictureFormat = \"jpeg\" | \"png\";\n\n/** Defines a standard picture size with width and height. */\nexport interface PictureSize {\n /** The width of the picture in pixels. */\n width: number;\n /** The height of the picture in pixels. */\n height: number;\n}\n\n/** Represents the supported picture sizes for a camera facing a certain direction. */\nexport interface SupportedPictureSizes {\n /** The camera direction (\"front\" or \"rear\"). */\n facing: string;\n /** A list of supported picture sizes for this camera. */\n supportedPictureSizes: PictureSize[];\n}\n\n/**\n * Defines the options for capturing a sample frame from the camera preview.\n */\nexport interface CameraSampleOptions {\n /**\n * The quality of the captured sample, from 0 to 100.\n * @default 85\n */\n quality?: number;\n}\n\n/**\n * The available flash modes for the camera.\n * 'torch' is a continuous light mode.\n */\nexport type CameraPreviewFlashMode = \"off\" | \"on\" | \"auto\" | \"torch\";\n\n/** Reusable exposure mode type for cross-platform support. */\nexport type ExposureMode = \"AUTO\" | \"LOCK\" | \"CONTINUOUS\" | \"CUSTOM\";\n\n/**\n * Defines the options for setting the camera preview's opacity.\n */\nexport interface CameraOpacityOptions {\n /**\n * The opacity percentage, from 0.0 (fully transparent) to 1.0 (fully opaque).\n * @default 1.0\n */\n opacity?: number;\n}\n\n/**\n * Represents safe area insets for devices.\n * Android: Values are expressed in logical pixels (dp) to match JS layout units.\n * iOS: Values are expressed in physical pixels and exclude status bar.\n */\nexport interface SafeAreaInsets {\n /** Current device orientation (1 = portrait, 2 = landscape, 0 = unknown). */\n orientation: number;\n /**\n * Orientation-aware notch/camera cutout inset (excluding status bar).\n * In portrait mode: returns top inset (notch at top).\n * In landscape mode: returns left inset (notch at side).\n * Android: Value in dp, iOS: Value in pixels (status bar excluded).\n */\n top: number;\n}\n\n/**\n * Canonical device orientation values across platforms.\n */\nexport type DeviceOrientation =\n | \"portrait\"\n | \"landscape-left\"\n | \"landscape-right\"\n | \"portrait-upside-down\"\n | \"unknown\";\n\n/**\n * The main interface for the CameraPreview plugin.\n */\nexport interface CameraPreviewPlugin {\n /**\n * Starts the camera preview.\n *\n * @param {CameraPreviewOptions} options - The configuration for the camera preview.\n * @returns {Promise<{ width: number; height: number; x: number; y: number }>} A promise that resolves with the preview dimensions.\n * @since 0.0.1\n */\n start(options: CameraPreviewOptions): Promise<{\n /** The width of the preview in pixels. */\n width: number;\n /** The height of the preview in pixels. */\n height: number;\n /** The horizontal origin of the preview, in pixels. */\n x: number;\n /** The vertical origin of the preview, in pixels. */\n y: number;\n }>;\n\n /**\n * Stops the camera preview.\n *\n * @returns {Promise<void>} A promise that resolves when the camera preview is stopped.\n * @since 0.0.1\n */\n stop(): Promise<void>;\n\n /**\n * Captures a picture from the camera.\n *\n * If `storeToFile` was set to `true` when starting the preview, the returned\n * `value` will be an absolute file path on the device instead of a base64 string. Use getBase64FromFilePath to get the base64 string from the file path.\n *\n * @param {CameraPreviewPictureOptions} options - The options for capturing the picture.\n * @returns {Promise<{ value: string; exif: ExifData }>} Resolves with:\n * - `value`: base64 string, or file path if `storeToFile` is true\n * - `exif`: extracted EXIF metadata when available\n * @since 0.0.1\n */\n capture(\n options: CameraPreviewPictureOptions,\n ): Promise<{ value: string; exif: ExifData }>;\n\n /**\n * Captures a single frame from the camera preview stream.\n *\n * @param {CameraSampleOptions} options - The options for capturing the sample.\n * @returns {Promise<{ value: string }>} A promise that resolves with the sample image as a base64 encoded string.\n * @since 0.0.1\n */\n captureSample(options: CameraSampleOptions): Promise<{ value: string }>;\n\n /**\n * Gets the flash modes supported by the active camera.\n *\n * @returns {Promise<{ result: CameraPreviewFlashMode[] }>} A promise that resolves with an array of supported flash modes.\n * @since 0.0.1\n */\n getSupportedFlashModes(): Promise<{\n result: CameraPreviewFlashMode[];\n }>;\n\n /**\n * Set the aspect ratio of the camera preview.\n *\n * @param {{ aspectRatio: '4:3' | '16:9'; x?: number; y?: number }} options - The desired aspect ratio and optional position.\n * - aspectRatio: The desired aspect ratio ('4:3' or '16:9')\n * - x: Optional x coordinate for positioning. If not provided, view will be auto-centered horizontally.\n * - y: Optional y coordinate for positioning. If not provided, view will be auto-centered vertically.\n * @returns {Promise<{ width: number; height: number; x: number; y: number }>} A promise that resolves with the actual preview dimensions and position.\n * @since 7.5.0\n * @platform android, ios\n */\n setAspectRatio(options: {\n aspectRatio: \"4:3\" | \"16:9\";\n x?: number;\n y?: number;\n }): Promise<{\n width: number;\n height: number;\n x: number;\n y: number;\n }>;\n\n /**\n * Gets the current aspect ratio of the camera preview.\n *\n * @returns {Promise<{ aspectRatio: '4:3' | '16:9' }>} A promise that resolves with the current aspect ratio.\n * @since 7.5.0\n * @platform android, ios\n */\n getAspectRatio(): Promise<{ aspectRatio: \"4:3\" | \"16:9\" }>;\n\n /**\n * Sets the grid mode of the camera preview overlay.\n *\n * @param {{ gridMode: GridMode }} options - The desired grid mode ('none', '3x3', or '4x4').\n * @returns {Promise<void>} A promise that resolves when the grid mode is set.\n * @since 8.0.0\n */\n setGridMode(options: { gridMode: GridMode }): Promise<void>;\n\n /**\n * Gets the current grid mode of the camera preview overlay.\n *\n * @returns {Promise<{ gridMode: GridMode }>} A promise that resolves with the current grid mode.\n * @since 8.0.0\n */\n getGridMode(): Promise<{ gridMode: GridMode }>;\n\n /**\n * Gets the horizontal field of view (FoV) for the active camera.\n * Note: This can be an estimate on some devices.\n *\n * @returns {Promise<{ result: number }>} A promise that resolves with the horizontal field of view in degrees.\n * @since 0.0.1\n */\n getHorizontalFov(): Promise<{\n result: number;\n }>;\n\n /**\n * Gets the supported picture sizes for all cameras.\n *\n * @returns {Promise<{ supportedPictureSizes: SupportedPictureSizes[] }>} A promise that resolves with the list of supported sizes.\n * @since 7.4.0\n */\n getSupportedPictureSizes(): Promise<{\n supportedPictureSizes: SupportedPictureSizes[];\n }>;\n\n /**\n * Sets the flash mode for the active camera.\n *\n * @param {{ flashMode: CameraPreviewFlashMode | string }} options - The desired flash mode.\n * @returns {Promise<void>} A promise that resolves when the flash mode is set.\n * @since 0.0.1\n */\n setFlashMode(options: {\n flashMode: CameraPreviewFlashMode | string;\n }): Promise<void>;\n\n /**\n * Toggles between the front and rear cameras.\n *\n * @returns {Promise<void>} A promise that resolves when the camera is flipped.\n * @since 0.0.1\n */\n flip(): Promise<void>;\n\n /**\n * Sets the opacity of the camera preview.\n *\n * @param {CameraOpacityOptions} options - The opacity options.\n * @returns {Promise<void>} A promise that resolves when the opacity is set.\n * @since 0.0.1\n */\n setOpacity(options: CameraOpacityOptions): Promise<void>;\n\n /**\n * Stops an ongoing video recording.\n *\n * @returns {Promise<{ videoFilePath: string }>} A promise that resolves with the path to the recorded video file.\n * @since 0.0.1\n */\n stopRecordVideo(): Promise<{ videoFilePath: string }>;\n\n /**\n * Starts recording a video.\n *\n * @param {CameraPreviewOptions} options - The options for video recording. Only iOS.\n * @returns {Promise<void>} A promise that resolves when video recording starts.\n * @since 0.0.1\n */\n startRecordVideo(options: CameraPreviewOptions): Promise<void>;\n\n /**\n * Checks if the camera preview is currently running.\n *\n * @returns {Promise<{ isRunning: boolean }>} A promise that resolves with the running state.\n * @since 7.5.0\n * @platform android, ios\n */\n isRunning(): Promise<{ isRunning: boolean }>;\n\n /**\n * Gets all available camera devices.\n *\n * @returns {Promise<{ devices: CameraDevice[] }>} A promise that resolves with the list of available camera devices.\n * @since 7.5.0\n * @platform android, ios\n */\n getAvailableDevices(): Promise<{ devices: CameraDevice[] }>;\n\n /**\n * Gets the current zoom state, including min/max and current lens info.\n *\n * @returns {Promise<{ min: number; max: number; current: number; lens: LensInfo }>} A promise that resolves with the zoom state.\n * @since 7.5.0\n * @platform android, ios\n */\n getZoom(): Promise<{\n min: number;\n max: number;\n current: number;\n lens: LensInfo;\n }>;\n\n /**\n * Returns zoom button values for quick switching.\n * - iOS/Android: includes 0.5 if ultra-wide available; 1 and 2 if wide available; 3 if telephoto available\n * - Web: unsupported\n * @since 7.5.0\n * @platform android, ios\n */\n getZoomButtonValues(): Promise<{ values: number[] }>;\n\n /**\n * Sets the zoom level of the camera.\n *\n * @param {{ level: number; ramp?: boolean; autoFocus?: boolean }} options - The desired zoom level. `ramp` is currently unused. `autoFocus` defaults to true.\n * @returns {Promise<void>} A promise that resolves when the zoom level is set.\n * @since 7.5.0\n * @platform android, ios\n */\n setZoom(options: {\n level: number;\n ramp?: boolean;\n autoFocus?: boolean;\n }): Promise<void>;\n\n /**\n * Gets the current flash mode.\n *\n * @returns {Promise<{ flashMode: FlashMode }>} A promise that resolves with the current flash mode.\n * @since 7.5.0\n * @platform android, ios\n */\n getFlashMode(): Promise<{ flashMode: FlashMode }>;\n\n /**\n * Removes all registered listeners.\n *\n * @since 7.5.0\n * @platform android, ios\n */\n removeAllListeners(): Promise<void>;\n\n /**\n * Switches the active camera to the one with the specified `deviceId`.\n *\n * @param {{ deviceId: string }} options - The ID of the device to switch to.\n * @returns {Promise<void>} A promise that resolves when the camera is switched.\n * @since 7.5.0\n * @platform android, ios\n */\n setDeviceId(options: { deviceId: string }): Promise<void>;\n\n /**\n * Gets the ID of the currently active camera device.\n *\n * @returns {Promise<{ deviceId: string }>} A promise that resolves with the current device ID.\n * @since 7.5.0\n * @platform android, ios\n */\n getDeviceId(): Promise<{ deviceId: string }>;\n\n /**\n * Gets the current preview size and position.\n * @returns {Promise<{x: number, y: number, width: number, height: number}>}\n * @since 7.5.0\n * @platform android, ios\n */\n getPreviewSize(): Promise<{\n x: number;\n y: number;\n width: number;\n height: number;\n }>;\n /**\n * Sets the preview size and position.\n * @param options The new position and dimensions.\n * @returns {Promise<{ width: number; height: number; x: number; y: number }>} A promise that resolves with the actual preview dimensions and position.\n * @since 7.5.0\n * @platform android, ios\n */\n setPreviewSize(options: {\n x?: number;\n y?: number;\n width: number;\n height: number;\n }): Promise<{\n width: number;\n height: number;\n x: number;\n y: number;\n }>;\n\n /**\n * Sets the camera focus to a specific point in the preview.\n *\n * Note: The plugin does not attach any native tap-to-focus gesture handlers. Handle taps in\n * your HTML/JS (e.g., on the overlaying UI), then pass normalized coordinates here.\n *\n * @param {Object} options - The focus options.\n * @param {number} options.x - The x coordinate in the preview view to focus on (0-1 normalized).\n * @param {number} options.y - The y coordinate in the preview view to focus on (0-1 normalized).\n * @returns {Promise<void>} A promise that resolves when the focus is set.\n * @since 7.5.0\n * @platform android, ios\n */\n setFocus(options: { x: number; y: number }): Promise<void>;\n\n /**\n * Adds a listener for screen resize events.\n * @param {string} eventName - The event name to listen for.\n * @param {Function} listenerFunc - The function to call when the event is triggered.\n * @returns {Promise<PluginListenerHandle>} A promise that resolves with a handle to the listener.\n * @since 7.5.0\n * @platform android, ios\n */\n addListener(\n eventName: \"screenResize\",\n listenerFunc: (data: {\n width: number;\n height: number;\n x: number;\n y: number;\n }) => void,\n ): Promise<PluginListenerHandle>;\n\n /**\n * Adds a listener for orientation change events.\n * @param {string} eventName - The event name to listen for.\n * @param {Function} listenerFunc - The function to call when the event is triggered.\n * @returns {Promise<PluginListenerHandle>} A promise that resolves with a handle to the listener.\n * @since 7.5.0\n * @platform android, ios\n */\n addListener(\n eventName: \"orientationChange\",\n listenerFunc: (data: { orientation: DeviceOrientation }) => void,\n ): Promise<PluginListenerHandle>;\n /**\n * Deletes a file at the given absolute path on the device.\n * Use this to quickly clean up temporary images created with `storeToFile`.\n * On web, this is not supported and will throw.\n * @since 7.5.0\n * @platform android, ios\n */\n deleteFile(options: { path: string }): Promise<{ success: boolean }>;\n\n /**\n * Gets the safe area insets for devices.\n * Returns the orientation-aware notch/camera cutout inset and the current orientation.\n * In portrait mode: returns top inset (notch at top).\n * In landscape mode: returns left inset (notch moved to side).\n * This specifically targets the cutout area (notch, punch hole, etc.) that all modern phones have.\n *\n * Android: Values returned in dp (logical pixels).\n * iOS: Values returned in physical pixels, excluding status bar (only pure notch/cutout size).\n *\n * @platform android, ios\n */\n getSafeAreaInsets(): Promise<SafeAreaInsets>;\n\n /**\n * Gets the current device orientation in a cross-platform format.\n * @since 7.5.0\n * @platform android, ios\n */\n getOrientation(): Promise<{ orientation: DeviceOrientation }>;\n\n /**\n * Returns the exposure modes supported by the active camera.\n * Modes can include: 'locked', 'auto', 'continuous', 'custom'.\n * @platform android, ios\n */\n getExposureModes(): Promise<{ modes: ExposureMode[] }>;\n\n /**\n * Returns the current exposure mode.\n * @platform android, ios\n */\n getExposureMode(): Promise<{ mode: ExposureMode }>;\n\n /**\n * Sets the exposure mode.\n * @platform android, ios\n */\n setExposureMode(options: { mode: ExposureMode }): Promise<void>;\n\n /**\n * Returns the exposure compensation (EV bias) supported range.\n * @platform ios\n */\n getExposureCompensationRange(): Promise<{\n min: number;\n max: number;\n step: number;\n }>;\n\n /**\n * Returns the current exposure compensation (EV bias).\n * @platform ios\n */\n getExposureCompensation(): Promise<{ value: number }>;\n\n /**\n * Sets the exposure compensation (EV bias). Value will be clamped to range.\n * @platform ios\n */\n setExposureCompensation(options: { value: number }): Promise<void>;\n}\n"]}
@@ -1,6 +1,7 @@
1
1
  import AVFoundation
2
2
  import UIKit
3
3
  import CoreLocation
4
+ import UniformTypeIdentifiers
4
5
 
5
6
  class CameraController: NSObject {
6
7
  private func getVideoOrientation() -> AVCaptureVideoOrientation {
@@ -888,7 +889,7 @@ extension CameraController {
888
889
  self.updateVideoOrientation()
889
890
  }
890
891
 
891
- func captureImage(width: Int?, height: Int?, quality: Float, gpsLocation: CLLocation?, completion: @escaping (UIImage?, Data?, [AnyHashable: Any]?, Error?) -> Void) {
892
+ func captureImage(width: Int?, height: Int?, quality: Float, gpsLocation: CLLocation?, embedTimestamp: Bool, completion: @escaping (UIImage?, Data?, [AnyHashable: Any]?, Error?) -> Void) {
892
893
  guard let photoOutput = self.photoOutput else {
893
894
  completion(nil, nil, nil, NSError(domain: "Camera", code: 0, userInfo: [NSLocalizedDescriptionKey: "Photo output is not available"]))
894
895
  return
@@ -972,7 +973,14 @@ extension CameraController {
972
973
  print("[CameraPreview] Applied aspect ratio cropping for \(aspectRatio): \(finalImage.size.width)x\(finalImage.size.height)")
973
974
  }
974
975
 
976
+ // Embed timestamp if requested
977
+ if embedTimestamp {
978
+ let when = self.makeTimestampString(from: photoData, metadata: metadata)
979
+ finalImage = self.drawTimestamp(text: when, on: finalImage)
980
+ }
981
+
975
982
  completion(finalImage, photoData, metadata, nil)
983
+
976
984
  // End capture lifecycle
977
985
  self.isCapturingPhoto = false
978
986
  if self.stopRequestedAfterCapture {
@@ -983,6 +991,172 @@ extension CameraController {
983
991
  photoOutput.capturePhoto(with: settings, delegate: self)
984
992
  }
985
993
 
994
+ /// Draws a timestamp bubble at the top-right of the image and returns a new image.
995
+ func drawTimestamp(text: String, on image: UIImage) -> UIImage {
996
+ let textColor: UIColor = .white
997
+ // Slightly lighter/clearer than before (matches iOS feel better)
998
+ let backgroundColor: UIColor = UIColor(white: 0.12, alpha: 0.22)
999
+ let paddingH: CGFloat = 16 // horizontal
1000
+ let paddingV: CGFloat = 10 // vertical
1001
+ let cornerRadius: CGFloat = 10
1002
+ let drawsShadow = true
1003
+
1004
+ let base = image.fixedOrientation() ?? image
1005
+ let scale = base.scale
1006
+ let size = base.size
1007
+
1008
+ // ≈3.5% of image width (clamped to ≥10pt)
1009
+ let fontPointSize: CGFloat = max(10, size.width * 0.035)
1010
+ // San Francisco system font is what the Camera/Photos overlays use
1011
+ let font: UIFont = .systemFont(ofSize: fontPointSize, weight: .semibold)
1012
+
1013
+ let attrs: [NSAttributedString.Key: Any] = [
1014
+ .font: font,
1015
+ .foregroundColor: textColor
1016
+ ]
1017
+ let textSize = (text as NSString).size(withAttributes: attrs)
1018
+
1019
+ // Bubble rect (top-right)
1020
+ let bgSize = CGSize(width: textSize.width + paddingH * 2,
1021
+ height: textSize.height + paddingV * 2)
1022
+ let margin: CGFloat = 12 // distance from edges of the photo
1023
+ let bgOrigin = CGPoint(x: size.width - bgSize.width - margin,
1024
+ y: margin)
1025
+ let bgRect = CGRect(origin: bgOrigin, size: bgSize)
1026
+
1027
+ // Text origin inside bubble
1028
+ let textOrigin = CGPoint(x: bgRect.minX + paddingH,
1029
+ y: bgRect.minY + paddingV)
1030
+
1031
+ let format = UIGraphicsImageRendererFormat.default()
1032
+ format.scale = scale
1033
+ format.opaque = true
1034
+
1035
+ return UIGraphicsImageRenderer(size: size, format: format).image { ctx in
1036
+ base.draw(in: CGRect(origin: .zero, size: size))
1037
+
1038
+ // Bubble
1039
+ let bubblePath = UIBezierPath(roundedRect: bgRect, cornerRadius: cornerRadius)
1040
+ if drawsShadow {
1041
+ ctx.cgContext.saveGState()
1042
+ ctx.cgContext.setShadow(offset: CGSize(width: 0, height: 2),
1043
+ blur: 6,
1044
+ color: UIColor.black.withAlphaComponent(0.25).cgColor)
1045
+ backgroundColor.setFill()
1046
+ bubblePath.fill()
1047
+ ctx.cgContext.restoreGState()
1048
+ } else {
1049
+ backgroundColor.setFill()
1050
+ bubblePath.fill()
1051
+ }
1052
+
1053
+ // High-quality text rendering
1054
+ let g = ctx.cgContext
1055
+ g.setAllowsAntialiasing(true)
1056
+ g.setShouldAntialias(true)
1057
+ g.setAllowsFontSmoothing(true)
1058
+ g.setShouldSmoothFonts(true)
1059
+ g.setShouldSubpixelPositionFonts(true)
1060
+ g.interpolationQuality = .high
1061
+
1062
+ // Single pass: fill only (no stroke/outline)
1063
+ (text as NSString).draw(at: textOrigin, withAttributes: attrs)
1064
+ }
1065
+ }
1066
+
1067
+ func makeTimestampString(from photoData: Data?, metadata: [AnyHashable: Any]?) -> String {
1068
+ func extractDateString(from meta: [String: Any]) -> String? {
1069
+ if let exif = meta[kCGImagePropertyExifDictionary as String] as? [String: Any] {
1070
+ if let s = exif[kCGImagePropertyExifDateTimeOriginal as String] as? String { return s }
1071
+ if let s = exif[kCGImagePropertyExifDateTimeDigitized as String] as? String { return s }
1072
+ }
1073
+ if let tiff = meta[kCGImagePropertyTIFFDictionary as String] as? [String: Any] {
1074
+ if let s = tiff[kCGImagePropertyTIFFDateTime as String] as? String { return s }
1075
+ }
1076
+ return nil
1077
+ }
1078
+
1079
+ var raw: String?
1080
+ if let metadata = metadata as? [String: Any] {
1081
+ raw = extractDateString(from: metadata)
1082
+ }
1083
+ if raw == nil, let data = photoData,
1084
+ let src = CGImageSourceCreateWithData(data as CFData, nil),
1085
+ let props = CGImageSourceCopyPropertiesAtIndex(src, 0, nil) as? [String: Any] {
1086
+ raw = extractDateString(from: props)
1087
+ }
1088
+
1089
+ let outFmt = DateFormatter()
1090
+ outFmt.locale = .current
1091
+ outFmt.timeZone = .current
1092
+ outFmt.dateFormat = "yyyy-MM-dd HH:mm:ss"
1093
+
1094
+ if let raw = raw {
1095
+ let df = DateFormatter()
1096
+ df.locale = Locale(identifier: "en_US_POSIX")
1097
+ df.timeZone = .current
1098
+ df.dateFormat = raw.contains(".") ? "yyyy:MM:dd HH:mm:ss.SSS" : "yyyy:MM:dd HH:mm:ss"
1099
+ if let d = df.date(from: raw) {
1100
+ return outFmt.string(from: d)
1101
+ }
1102
+ }
1103
+
1104
+ return outFmt.string(from: Date())
1105
+ }
1106
+
1107
+ // Create JPEG data from `image`, merging the original EXIF/GPS/etc. and forcing Orientation=1.
1108
+ func jpegDataPreservingMetadata(from image: UIImage,
1109
+ originalPhotoData: Data?,
1110
+ originalMetadata: [AnyHashable: Any]?,
1111
+ quality: CGFloat = 0.9) -> Data? {
1112
+ // Encode pixels first
1113
+ guard let cgImg = image.cgImage else { return image.jpegData(compressionQuality: quality) }
1114
+ let uiImageData = UIImage(cgImage: cgImg, scale: image.scale, orientation: .up)
1115
+ .jpegData(compressionQuality: quality)
1116
+
1117
+ // If we don’t have source metadata, just return the new JPEG
1118
+ guard let srcData = originalPhotoData, let newJPEG = uiImageData else { return uiImageData }
1119
+
1120
+ // Load base metadata from source, then overlay any explicit metadata dict we were given
1121
+ let cgSrc = CGImageSourceCreateWithData(srcData as CFData, nil)
1122
+ let baseMetadata: [String: Any]
1123
+ if let src = cgSrc,
1124
+ let props = CGImageSourceCopyPropertiesAtIndex(src, 0, nil) as? [String: Any] {
1125
+ var merged = props
1126
+ if let explicit = originalMetadata as? [String: Any] {
1127
+ for (k, v) in explicit { merged[k] = v }
1128
+ }
1129
+ baseMetadata = merged
1130
+ } else if let explicit = originalMetadata as? [String: Any] {
1131
+ baseMetadata = explicit
1132
+ } else {
1133
+ return newJPEG
1134
+ }
1135
+
1136
+ // Prepare destination
1137
+ let dstData = NSMutableData()
1138
+ guard let cgDst = CGImageDestinationCreateWithData(dstData, UTType.jpeg.identifier as CFString, 1, nil) else {
1139
+ return newJPEG
1140
+ }
1141
+
1142
+ // Force normalized orientation (pixels are already .up)
1143
+ var metaOut = baseMetadata
1144
+ if var tiff = metaOut[kCGImagePropertyTIFFDictionary as String] as? [String: Any] {
1145
+ tiff[kCGImagePropertyTIFFOrientation as String] = 1
1146
+ metaOut[kCGImagePropertyTIFFDictionary as String] = tiff
1147
+ }
1148
+ metaOut[kCGImagePropertyOrientation as String] = 1
1149
+
1150
+ // Write the new pixels + merged metadata
1151
+ if let cgImage = UIImage(data: newJPEG)?.cgImage {
1152
+ CGImageDestinationAddImage(cgDst, cgImage, metaOut as CFDictionary)
1153
+ CGImageDestinationFinalize(cgDst)
1154
+ return (dstData as Data)
1155
+ }
1156
+
1157
+ return newJPEG
1158
+ }
1159
+
986
1160
  func addGPSMetadata(to image: UIImage, location: CLLocation) {
987
1161
  guard let jpegData = image.jpegData(compressionQuality: 1.0),
988
1162
  let source = CGImageSourceCreateWithData(jpegData as CFData, nil),
@@ -855,12 +855,13 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
855
855
  let quality = call.getFloat("quality", 85)
856
856
  let saveToGallery = call.getBool("saveToGallery", false)
857
857
  let withExifLocation = call.getBool("withExifLocation", false)
858
+ let embedTimestamp = call.getBool("embedTimestamp", false) ?? false
858
859
  let width = call.getInt("width")
859
860
  let height = call.getInt("height")
860
861
 
861
862
  print("[CameraPreview] Raw parameter values - width: \(String(describing: width)), height: \(String(describing: height))")
862
863
 
863
- print("[CameraPreview] Capture params - quality: \(quality), saveToGallery: \(saveToGallery), withExifLocation: \(withExifLocation), width: \(width ?? -1), height: \(height ?? -1)")
864
+ print("[CameraPreview] Capture params - quality: \(quality), saveToGallery: \(saveToGallery), withExifLocation: \(withExifLocation), embedTimestamp: \(embedTimestamp), width: \(width ?? -1), height: \(height ?? -1)")
864
865
  print("[CameraPreview] Current location: \(self.currentLocation?.description ?? "nil")")
865
866
  // Safely read frame from main thread for logging
866
867
  let (previewWidth, previewHeight): (CGFloat, CGFloat) = {
@@ -877,7 +878,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
877
878
  }()
878
879
  print("[CameraPreview] Preview dimensions: \(previewWidth)x\(previewHeight)")
879
880
 
880
- self.cameraController.captureImage(width: width, height: height, quality: quality, gpsLocation: self.currentLocation) { (image, originalPhotoData, _, error) in
881
+ self.cameraController.captureImage(width: width, height: height, quality: quality, gpsLocation: self.currentLocation, embedTimestamp: embedTimestamp) { (image, originalPhotoData, _, error) in
881
882
  print("[CameraPreview] captureImage callback received")
882
883
  DispatchQueue.main.async {
883
884
  print("[CameraPreview] Processing capture on main thread")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/camera-preview",
3
- "version": "7.16.4",
3
+ "version": "7.17.1",
4
4
  "description": "Camera preview",
5
5
  "license": "MIT",
6
6
  "repository": {