Skip to content

Commit

Permalink
340 ocr feature request (#434)
Browse files Browse the repository at this point in the history
* Added vision feature

* Refactor background index.ts

* Fixed error handling

* Add dependencies for Firebase functions, core common, and core service
  • Loading branch information
dharmesh-hemaram authored Sep 20, 2024
1 parent 84d95e4 commit 6679a7d
Show file tree
Hide file tree
Showing 22 changed files with 268 additions and 63 deletions.
23 changes: 4 additions & 19 deletions apps/acf-extension/src/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,20 @@ import { GoogleDriveBackground, RUNTIME_MESSAGE_GOOGLE_DRIVE } from '@dhruv-tech
import { GoogleOauth2Background, RUNTIME_MESSAGE_GOOGLE_OAUTH } from '@dhruv-techapps/google-oauth';
import { GoogleSheetsBackground, RUNTIME_MESSAGE_GOOGLE_SHEETS } from '@dhruv-techapps/google-sheets';
import { registerNotifications } from '@dhruv-techapps/notifications';
import { RUNTIME_MESSAGE_VISION, VisionBackground } from '@dhruv-techapps/vision';
import XMLHttpRequest from 'xhr-shim';
import { ACTION_POPUP } from '../common/constant';
import { DISCORD_CLIENT_ID, EDGE_OAUTH_CLIENT_ID, FIREBASE_FUNCTIONS_URL, OPTIONS_PAGE_URL, UNINSTALL_URL, VARIANT } from '../common/environments';
import { DISCORD_CLIENT_ID, EDGE_OAUTH_CLIENT_ID, FIREBASE_FUNCTIONS_URL, OPTIONS_PAGE_URL, VARIANT } from '../common/environments';
import AcfBackup from './acf-backup';
import registerContextMenus from './context-menu';
import { auth } from './firebase';
import { googleAnalytics } from './google-analytics';
import './sync-config';
import { TabsMessenger } from './tab';
import { Update } from './update';
self['XMLHttpRequest'] = XMLHttpRequest;

let firebaseFirestoreBackground: FirebaseFirestoreBackground;
self['XMLHttpRequest'] = XMLHttpRequest;

try {
firebaseFirestoreBackground = new FirebaseFirestoreBackground(auth, EDGE_OAUTH_CLIENT_ID, OPTIONS_PAGE_URL);

/**
* Browser Action set to open option page / configuration page
*/
Expand All @@ -46,12 +43,6 @@ try {
}
});

auth.onAuthStateChanged((user) => {
if (user) {
Update.discord(firebaseFirestoreBackground);
}
});

/**
* Set Context Menu for right click
*/
Expand All @@ -62,13 +53,6 @@ try {
*/
registerNotifications(OPTIONS_PAGE_URL);

/**
* Setup Uninstall action
*/
if (UNINSTALL_URL) {
chrome.runtime.setUninstallURL(UNINSTALL_URL);
}

/**
* Setup on Message Listener
*/
Expand All @@ -85,6 +69,7 @@ try {
[RUNTIME_MESSAGE_FIREBASE_OAUTH]: new FirebaseOauth2Background(auth, EDGE_OAUTH_CLIENT_ID),
[RUNTIME_MESSAGE_FIREBASE_FIRESTORE]: new FirebaseFirestoreBackground(auth, EDGE_OAUTH_CLIENT_ID, OPTIONS_PAGE_URL),
[RUNTIME_MESSAGE_FIREBASE_FUNCTIONS]: new FirebaseFunctionsBackground(auth, FIREBASE_FUNCTIONS_URL, EDGE_OAUTH_CLIENT_ID),
[RUNTIME_MESSAGE_VISION]: new VisionBackground(auth, FIREBASE_FUNCTIONS_URL, EDGE_OAUTH_CLIENT_ID),
};
Runtime.onMessageExternal(onMessageListener);
Runtime.onMessage(onMessageListener);
Expand Down
14 changes: 0 additions & 14 deletions apps/acf-extension/src/background/update.ts

This file was deleted.

2 changes: 1 addition & 1 deletion apps/acf-extension/src/content_scripts/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const ActionProcessor = (() => {
if (elements === undefined) {
return ACTION_STATUS.SKIPPED;
}
const value = action.value ? await ACFValue.getValue(action.value) : action.value;
const value = action.value ? await ACFValue.getValue(action.value, action.settings) : action.value;
await ACFEvents.check(elementFinder, elements, value);
};

Expand Down
23 changes: 22 additions & 1 deletion apps/acf-extension/src/content_scripts/util/acf-value.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,42 @@
import { ActionSettings } from '@dhruv-techapps/acf-common';
import { Value } from '@dhruv-techapps/acf-util';
import { GoogleSheetsValue } from '@dhruv-techapps/google-sheets';
import { SandboxValue } from '@dhruv-techapps/sandbox';
import { VisionService, VisionValue } from '@dhruv-techapps/vision';
import Common from '../common';

export const VALUE_MATCHER = {
GOOGLE_SHEETS: /^GoogleSheets::/i,
FUNC: /^Func::/i,
IMAGE: /^Image::/i,
};

export class ACFValue {
static async getValue(value: string) {
static async getValue(value: string, settings?: ActionSettings): Promise<string> {
value = await Value.getValue(value);
if (VALUE_MATCHER.GOOGLE_SHEETS.test(value)) {
value = GoogleSheetsValue.getSheetValue(value);
}
if (VALUE_MATCHER.FUNC.test(value)) {
value = await SandboxValue.getFuncValue(value);
}
if (VALUE_MATCHER.IMAGE.test(value)) {
try {
const elementFinder = value.replace(/image::/i, '');
const elements = await Common.start(elementFinder, settings);
if (elements === undefined || typeof elements === 'number' || elements.length === 0) {
throw new Error('No element found with the given selector');
}
const data = VisionValue.getImageSrc(elements);
value = await VisionService.imagesAnnotate(data);
return value;
} catch (error) {
if (error instanceof Error) {
return error.message;
}
}
return value;
}
return value;
}
}
6 changes: 6 additions & 0 deletions apps/acf-options-page/src/components/data-list.components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,12 @@ export function DataList() {
<option value='Element::scrollIntoView::{ "behavior": "smooth", "block": "end", "inline": "nearest" }'>
Element::scrollIntoView::&#123; "behavior": "smooth", "block": "end", "inline": "nearest" &#123;
</option>
<option value='Image::'>Image::</option>
<option value='Image::#'>Image::#</option>
<option value='Image::ClassName::'>Image::ClassName::</option>
<option value='Image::Name::'>Image::Name::</option>
<option value='Image::Selector::'>Image::Selector::</option>
<option value='Image:://input[@id="element-id"]'>Image:://input[@id=&quot;element-id&quot;]</option>
</datalist>
<datalist id='valueExtractor'>
<option value='@id'>To get id attribute of element</option>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Auth, FirebaseOauth2Background } from '@dhruv-techapps/firebase-oauth';
import { GOOGLE_SCOPES } from '@dhruv-techapps/google-oauth';
import { NotificationHandler } from '@dhruv-techapps/notifications';
import { NOTIFICATIONS_ID, NOTIFICATIONS_TITLE } from './firebase-functions.constant';
import { NOTIFICATIONS_ID } from './firebase-functions.constant';

export class FirebaseFunctionsBackground extends FirebaseOauth2Background {
cloudFunctionUrl: string;
Expand All @@ -11,11 +11,8 @@ export class FirebaseFunctionsBackground extends FirebaseOauth2Background {
this.cloudFunctionUrl = cloudFunctionUrl;
}

async visionImagesAnnotate<Res>(content: string): Promise<Res> {
async visionImagesAnnotate<Req, Res>(data: Req): Promise<Res> {
const headers = await this._getFirebaseHeaders();
const data = {
requests: [{ image: { content }, features: [{ type: 'TEXT_DETECTION' }] }],
};
const url = new URL(this.cloudFunctionUrl + '/visionImagesAnnotate');
const response = await this.#fetch(url, headers, data);
return response;
Expand Down Expand Up @@ -80,15 +77,21 @@ export class FirebaseFunctionsBackground extends FirebaseOauth2Background {
const response = await fetch(url.href, init);
const result = await response.json();
if (result.error) {
NotificationHandler.notify(NOTIFICATIONS_ID, NOTIFICATIONS_TITLE, result.error, true);
throw new Error(result.error);
throw new CustomError(result.error, result.message);
}
return result;
} catch (error) {
if (error instanceof Error) {
NotificationHandler.notify(NOTIFICATIONS_ID, NOTIFICATIONS_TITLE, error.message, true);
if (error instanceof CustomError || error instanceof Error) {
NotificationHandler.notify(NOTIFICATIONS_ID, error.name, error.message, true);
}
throw error;
}
}
}

class CustomError extends Error {
constructor(name: string, message: string) {
super(message);
this.name = name;
}
}
27 changes: 10 additions & 17 deletions libs/shared/google-sheets/src/lib/google-sheets-background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,17 @@ const NOTIFICATIONS_ID = 'google-sheets';
export class GoogleSheetsBackground extends FirebaseFunctionsBackground {
scopes = [GOOGLE_SCOPES.SHEETS];
async getSheets({ spreadsheetId, ranges }: GoogleSheetsRequest): Promise<GoogleSheetsResponse> {
try {
if (!spreadsheetId || !ranges) {
throw new Error('spreadsheetId or ranges is not defined');
}
if (!spreadsheetId || !ranges) {
throw new Error('spreadsheetId or ranges is not defined');
}

const response = await this.getValues<GoogleSheetsRequest, GoogleSheetsResponse>({ spreadsheetId, ranges });
return response.filter((result) => {
if (result.error) {
NotificationHandler.notify(NOTIFICATIONS_ID, NOTIFICATIONS_TITLE, result.error.message);
return false;
}
return true;
});
} catch (error) {
if (error instanceof Error) {
NotificationHandler.notify(NOTIFICATIONS_ID, NOTIFICATIONS_TITLE, error.message);
const response = await this.getValues<GoogleSheetsRequest, GoogleSheetsResponse>({ spreadsheetId, ranges });
return response.filter((result) => {
if (result.error) {
NotificationHandler.notify(NOTIFICATIONS_ID, NOTIFICATIONS_TITLE, result.error.message);
return false;
}
throw error;
}
return true;
});
}
}
25 changes: 25 additions & 0 deletions libs/shared/vision/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"extends": ["../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.json"],
"parser": "jsonc-eslint-parser",
"rules": {
"@nx/dependency-checks": "error"
}
}
]
}
10 changes: 10 additions & 0 deletions libs/shared/vision/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* eslint-disable */
export default {
displayName: 'vision',
preset: '../../../jest.preset.js',
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../../coverage/libs/shared/vision',
};
13 changes: 13 additions & 0 deletions libs/shared/vision/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "@dhruv-techapps/vision",
"version": "0.0.1",
"dependencies": {
"tslib": "^2.3.0",
"@dhruv-techapps/firebase-functions": "*",
"@dhruv-techapps/core-common": "*",
"@dhruv-techapps/core-service": "*"
},
"type": "commonjs",
"main": "./src/index.js",
"typings": "./src/index.d.ts"
}
42 changes: 42 additions & 0 deletions libs/shared/vision/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "vision",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/shared/vision/src",
"projectType": "library",
"release": {
"version": {
"generatorOptions": {
"packageRoot": "dist/{projectRoot}",
"currentVersionResolver": "git-tag"
}
}
},
"tags": [],
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/libs/shared/vision",
"main": "libs/shared/vision/src/index.ts",
"tsConfig": "libs/shared/vision/tsconfig.lib.json",
"assets": []
}
},
"nx-release-publish": {
"options": {
"packageRoot": "dist/{projectRoot}"
}
},
"lint": {
"executor": "@nx/eslint:lint"
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/shared/vision/jest.config.ts"
}
}
}
}
5 changes: 5 additions & 0 deletions libs/shared/vision/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './lib/vision-background';
export * from './lib/vision-types';
export * from './lib/vision-value';
export * from './lib/vision.constant';
export * from './lib/vision.service';
13 changes: 13 additions & 0 deletions libs/shared/vision/src/lib/vision-background.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { FirebaseFunctionsBackground } from '@dhruv-techapps/firebase-functions';
import { VisionImageRequest, VisionImageResponse } from './vision-types';

export class VisionBackground extends FirebaseFunctionsBackground {
async imagesAnnotate({ content, imageUrl }: VisionImageRequest): Promise<string> {
if (!content && !imageUrl) {
throw new Error('No image data found');
}
const data: VisionImageRequest = { content, imageUrl };
const result = await this.visionImagesAnnotate<VisionImageRequest, VisionImageResponse>(data);
return result.responses[0].fullTextAnnotation.text;
}
}
8 changes: 8 additions & 0 deletions libs/shared/vision/src/lib/vision-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type VisionImageRequest = {
content: string;
imageUrl: string;
};

export type VisionImageResponse = {
responses: Array<{ fullTextAnnotation: { text: string } }>;
};
36 changes: 36 additions & 0 deletions libs/shared/vision/src/lib/vision-value.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { VisionImageRequest } from './vision-types';

export class VisionValue {
static isBase64Image(dataURL: string) {
const regex = /^data:image\/(png|jpeg|jpg|gif|webp|bmp|svg\+xml|x-icon);base64,/;
return regex.test(dataURL);
}

static extractBase64Data(dataURL: string) {
// Split the data URL at the comma
const base64Data = dataURL.split(',')[1];
return base64Data;
}

static getImageSrc(elements: Array<HTMLElement>): VisionImageRequest {
const element = elements[0];
if (!element) {
throw new Error('No element found');
}
if (element.tagName !== 'IMG') {
throw new Error('Element is not an image');
}
const src = element.getAttribute('src');
if (!src) {
throw new Error('No image found');
}
let content = '';
let imageUrl = '';
if (this.isBase64Image(src)) {
content = this.extractBase64Data(src);
} else {
imageUrl = src;
}
return { content, imageUrl };
}
}
1 change: 1 addition & 0 deletions libs/shared/vision/src/lib/vision.constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const RUNTIME_MESSAGE_VISION = 'vision';
Loading

0 comments on commit 6679a7d

Please sign in to comment.