xref: /expo/packages/expo-clipboard/src/web/Utils.ts (revision 965e00ba)
1/**
2 * Converts base64-encoded data to a `Blob` object.
3 * @see https://stackoverflow.com/a/20151856
4 */
5export function base64toBlob(base64Data: string, contentType: string): Blob {
6  contentType = contentType || '';
7  const sliceSize = 1024;
8  const byteCharacters = atob(base64Data);
9  const bytesLength = byteCharacters.length;
10  const slicesCount = Math.ceil(bytesLength / sliceSize);
11  const byteArrays = new Array(slicesCount);
12
13  for (let sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) {
14    const begin = sliceIndex * sliceSize;
15    const end = Math.min(begin + sliceSize, bytesLength);
16
17    const bytes = new Array(end - begin);
18    for (let offset = begin, i = 0; offset < end; ++i, ++offset) {
19      bytes[i] = byteCharacters[offset].charCodeAt(0);
20    }
21    byteArrays[sliceIndex] = new Uint8Array(bytes);
22  }
23  // I cannot use `@ts-expect-error` here because some environments consider this correct:
24  // expo-module build - OK,
25  // expo-module test - error
26  // @ts-ignore `Blob` from `lib.dom.d.ts` and the one from `@types/react-native` differ somehow
27  return new Blob(byteArrays, { type: contentType });
28}
29
30/**
31 * Converts blob to base64-encoded string with Data-URL prefix.
32 */
33export function blobToBase64Async(blob: Blob): Promise<string> {
34  return new Promise((resolve, _) => {
35    const reader = new FileReader();
36    reader.onloadend = () => resolve(reader.result as string);
37    reader.readAsDataURL(blob);
38  });
39}
40
41export function htmlToPlainText(html: string) {
42  const tempDivElement = document.createElement('div');
43  tempDivElement.innerHTML = html;
44  return tempDivElement.textContent || tempDivElement.innerText || '';
45}
46
47export function getImageSizeFromBlobAsync(blob: Blob): Promise<{ width: number; height: number }> {
48  return new Promise((resolve, _) => {
49    const blobUrl = URL.createObjectURL(blob);
50    const img = document.createElement('img');
51    img.src = blobUrl;
52    img.onload = function () {
53      resolve({ width: img.width, height: img.height });
54    };
55  });
56}
57
58export async function findImageInClipboardAsync(items: ClipboardItems): Promise<Blob | null> {
59  for (const clipboardItem of items) {
60    // first look for png
61    if (clipboardItem.types.some((type) => type === 'image/png')) {
62      return await clipboardItem.getType('image/png');
63    }
64
65    // alternatively, an image might be a jpeg
66    // NOTE: Currently, this is not supported by browsers yet. They only support PNG now
67    if (clipboardItem.types.some((type) => type === 'image/jpeg')) {
68      return await clipboardItem.getType('image/jpeg');
69    }
70  }
71  return null;
72}
73
74export async function findHtmlInClipboardAsync(items: ClipboardItems): Promise<Blob | null> {
75  for (const clipboardItem of items) {
76    if (clipboardItem.types.some((type) => type === 'text/html')) {
77      return await clipboardItem.getType('text/html');
78    }
79  }
80  return null;
81}
82
83export async function isClipboardPermissionDeniedAsync(): Promise<boolean> {
84  const queryOpts = { name: 'clipboard-read' as PermissionName };
85  const permissionStatus = await navigator.permissions.query(queryOpts);
86  return permissionStatus.state === 'denied';
87}
88