Skip to content
This repository was archived by the owner on Aug 16, 2024. It is now read-only.

Commit 72b58ed

Browse files
committed
feat: ocr text for translating
1 parent fe4db1f commit 72b58ed

File tree

16 files changed

+631
-53
lines changed

16 files changed

+631
-53
lines changed

src/common/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ export interface Config {
44
token: string
55
targetLang: SupportLanguageKeys
66
region: APIRegions
7+
ocrSecretId?: string
8+
ocrSecretKey?: string
79
}
810

911
export type SupportLanguageKeys = keyof typeof supportedLanguages

src/components/svg/CursorClick.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import React from 'react'
2+
3+
export default function CursorClick(): React.ReactElement {
4+
return (
5+
<svg
6+
xmlns="http://www.w3.org/2000/svg"
7+
fill="none"
8+
width="16"
9+
height="16"
10+
viewBox="0 0 24 24"
11+
stroke="currentColor">
12+
<path
13+
strokeLinecap="round"
14+
strokeLinejoin="round"
15+
strokeWidth={2}
16+
d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"
17+
/>
18+
</svg>
19+
)
20+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React, { SVGProps } from 'react'
2+
import tw, { css } from 'twin.macro'
3+
4+
const LoadingCircle: React.FC<SVGProps<SVGSVGElement>> = (props) => {
5+
return (
6+
<svg
7+
{...props}
8+
xmlns="http://www.w3.org/2000/svg"
9+
fill="none"
10+
width="16"
11+
height="16"
12+
viewBox="0 0 24 24">
13+
<circle
14+
tw="opacity-25"
15+
cx="12"
16+
cy="12"
17+
r="10"
18+
stroke="currentColor"
19+
strokeWidth="4"
20+
/>
21+
<path
22+
tw="opacity-75"
23+
fill="currentColor"
24+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
25+
/>
26+
</svg>
27+
)
28+
}
29+
30+
export default LoadingCircle

src/global.d.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,25 @@ declare module 'rangy' {
3232

3333
export = rangy
3434
}
35+
36+
declare module 'react-resizable' {
37+
import { Axis, ResizeCallbackData, ResizeHandle } from 'react-resizable'
38+
39+
export interface ResizableProps {
40+
className?: string
41+
width: number
42+
height: number
43+
handle?: React.ReactNode | ((resizeHandle: ResizeHandle) => React.ReactNode)
44+
handleSize?: [number, number]
45+
lockAspectRatio?: boolean
46+
axis?: Axis
47+
minConstraints?: [number, number]
48+
maxConstraints?: [number, number]
49+
onResizeStop?: (e: React.SyntheticEvent, data: ResizeCallbackData) => any
50+
onResizeStart?: (e: React.SyntheticEvent, data: ResizeCallbackData) => any
51+
onResize?: (e: React.SyntheticEvent, data: ResizeCallbackData) => any
52+
draggableOpts?: any
53+
resizeHandles?: ResizeHandle[]
54+
style?: Record<string, any>
55+
}
56+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import defaultsDeep from 'lodash-es/defaultsDeep'
2+
import { SHA256, HmacSHA256, enc } from 'crypto-js'
3+
import axios from 'axios'
4+
5+
export class TcRequestError extends Error {
6+
code?: string
7+
8+
constructor(message: string, code?: string) {
9+
super(message + (code ? ` [${code}]` : ''))
10+
this.name = 'TcRequestError'
11+
if (code) this.code = code
12+
}
13+
}
14+
15+
export class OcrClient {
16+
private config: {
17+
secretId: string
18+
secretKey: string
19+
region: string
20+
}
21+
private requestConfig = {
22+
host: 'ocr.tencentcloudapi.com',
23+
service: 'ocr',
24+
version: '2018-11-19',
25+
algorithm: 'TC3-HMAC-SHA256',
26+
httpRequestMethod: 'POST',
27+
canonicalUri: '/',
28+
canonicalQueryString: '',
29+
canonicalHeaders:
30+
'content-type:application/json; charset=utf-8\nhost:ocr.tencentcloudapi.com\n',
31+
signedHeaders: 'content-type;host',
32+
}
33+
34+
constructor(config: { secretId: string; secretKey: string; host?: string }) {
35+
this.config = defaultsDeep({}, config, {
36+
region: 'ap-shanghai',
37+
})
38+
}
39+
40+
async request(data: { dataUrl: string }): Promise<string[]> {
41+
const payload = {
42+
ImageBase64: data.dataUrl,
43+
}
44+
const signature = this.signPayload(payload)
45+
const headers = {
46+
Authorization: signature.authorization,
47+
'Content-Type': 'application/json; charset=UTF-8',
48+
'X-TC-Action': 'GeneralBasicOCR',
49+
'X-TC-Timestamp': signature.timestamp,
50+
'X-TC-Version': this.requestConfig.version,
51+
'X-TC-Region': this.config.region,
52+
'X-TC-RequestClient': `WXAPP_SDK_OcrSDK_1.1.0`,
53+
}
54+
55+
return axios
56+
.request({
57+
url: 'https://ocr.tencentcloudapi.com',
58+
data: payload,
59+
method: 'POST',
60+
headers,
61+
responseType: 'json',
62+
})
63+
.then((res) => {
64+
if (res.data?.Response?.Error) {
65+
const error = res.data.Response.Error
66+
throw new TcRequestError(error.Message, error.Code)
67+
}
68+
69+
if (!res.data?.Response?.TextDetections) {
70+
throw new TcRequestError('没有数据返回')
71+
}
72+
73+
const detections = res.data.Response.TextDetections
74+
const result: string[] = []
75+
76+
detections.forEach(
77+
(item: { DetectedText: string; AdvancedInfo: string }) => {
78+
const advanceInfo: {
79+
Parag: {
80+
ParagNo: number
81+
}
82+
} = JSON.parse(item.AdvancedInfo)
83+
const index = advanceInfo.Parag.ParagNo - 1
84+
85+
if (result[index]) {
86+
result[index] += ` ${item.DetectedText}`
87+
} else {
88+
result.push(item.DetectedText)
89+
}
90+
},
91+
)
92+
93+
return result
94+
})
95+
}
96+
97+
private signPayload(
98+
payload: Record<string, any>,
99+
): { authorization: string; timestamp: number } {
100+
const hashedRequestPayload = SHA256(JSON.stringify(payload))
101+
const canonicalRequest = [
102+
this.requestConfig.httpRequestMethod,
103+
this.requestConfig.canonicalUri,
104+
this.requestConfig.canonicalQueryString,
105+
this.requestConfig.canonicalHeaders,
106+
this.requestConfig.signedHeaders,
107+
hashedRequestPayload,
108+
].join('\n')
109+
const t = new Date()
110+
const date = t.toISOString().substr(0, 10)
111+
const timestamp = Math.round(t.getTime() / 1000)
112+
const credentialScope = `${date}/${this.requestConfig.service}/tc3_request`
113+
const hashedCanonicalRequest = SHA256(canonicalRequest)
114+
const stringToSign = [
115+
this.requestConfig.algorithm,
116+
timestamp,
117+
credentialScope,
118+
hashedCanonicalRequest,
119+
].join('\n')
120+
121+
const secretDate = HmacSHA256(date, `TC3${this.config.secretKey}`)
122+
const secretService = HmacSHA256(this.requestConfig.service, secretDate)
123+
const secretSigning = HmacSHA256('tc3_request', secretService)
124+
125+
const signature = enc.Hex.stringify(HmacSHA256(stringToSign, secretSigning))
126+
127+
return {
128+
authorization:
129+
`${this.requestConfig.algorithm} ` +
130+
`Credential=${this.config.secretId}/${credentialScope}, ` +
131+
`SignedHeaders=${this.requestConfig.signedHeaders}, ` +
132+
`Signature=${signature}`,
133+
timestamp,
134+
}
135+
}
136+
}

src/pages/Background/common/server.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import { createServer } from 'connect.io'
44
import Client from '../../../common/api'
55
import logger from '../../../common/logger'
66
import { Config } from '../../../common/types'
7+
import { OcrClient } from './ocr-client'
78
import { Handler } from './types'
9+
import { cropImage } from './utils'
810

911
const server = createServer()
1012

@@ -46,8 +48,83 @@ const onTranslate: Handler<{
4648
})
4749
}
4850

51+
const onScreenshot: Handler<{
52+
x: number
53+
y: number
54+
width: number
55+
height: number
56+
clientWidth: number
57+
clientHeight: number
58+
clientPixelRatio: number
59+
}> = (payload, resolve, reject) => {
60+
;(async () => {
61+
logger.debug(
62+
{
63+
payload,
64+
},
65+
'receive screenshot payload',
66+
)
67+
68+
const dataUrl: string = await cc(chrome.tabs, 'captureVisibleTab', null, {
69+
quality: 75,
70+
})
71+
72+
resolve({
73+
dataUrl: await cropImage(dataUrl, {
74+
...payload,
75+
imageWidth: payload.clientWidth,
76+
imageHeight: payload.clientHeight,
77+
imageRatio: payload.clientPixelRatio,
78+
}),
79+
})
80+
})().catch((err) => {
81+
logger.error({
82+
err,
83+
})
84+
reject({
85+
message: err.message,
86+
})
87+
})
88+
}
89+
90+
const onOCR: Handler<{
91+
dataUrl: string
92+
}> = (payload, resolve, reject) => {
93+
;(async () => {
94+
logger.debug(
95+
{
96+
payload,
97+
},
98+
'receive ocr payload',
99+
)
100+
101+
const config: Config = await cc(chrome.storage.sync, 'get')
102+
103+
if (!config.ocrSecretId || !config.ocrSecretKey) {
104+
return
105+
}
106+
107+
const client = new OcrClient({
108+
secretId: config.ocrSecretId,
109+
secretKey: config.ocrSecretKey,
110+
})
111+
const data = await client.request({ dataUrl: payload.dataUrl })
112+
113+
resolve(data)
114+
})().catch((err) => {
115+
logger.error({
116+
err,
117+
})
118+
reject({
119+
message: err.message,
120+
})
121+
})
122+
}
123+
49124
server.on('connect', (client) => {
50125
client.on('translate', onTranslate)
126+
client.on('screenshot', onScreenshot)
127+
client.on('ocr', onOCR)
51128
})
52129

53130
export default server

src/pages/Background/common/utils.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { data } from 'autoprefixer'
12
import cc from 'chrome-call'
23
import { createClient } from 'connect.io'
4+
35
import logger from '../../../common/logger'
46

57
export const openExtension = (): void => {
@@ -15,3 +17,54 @@ export const openExtension = (): void => {
1517
})
1618
})
1719
}
20+
21+
export const cropImage = async (
22+
dataUrl: string,
23+
config: {
24+
x: number
25+
y: number
26+
width: number
27+
height: number
28+
imageWidth: number
29+
imageHeight: number
30+
imageRatio: number
31+
},
32+
): Promise<string> => {
33+
const croppedCanvas = await new Promise<HTMLCanvasElement>(
34+
(resolve, reject) => {
35+
const canvas = document.createElement('canvas')
36+
const img = new Image()
37+
38+
canvas.width = config.width
39+
canvas.height = config.height
40+
img.onload = () => {
41+
const ctx = canvas.getContext('2d')
42+
43+
canvas.width = config.width
44+
canvas.height = config.height
45+
46+
ctx?.drawImage(
47+
img,
48+
config.x * config.imageRatio,
49+
config.y * config.imageRatio,
50+
config.width * config.imageRatio,
51+
config.height * config.imageRatio,
52+
0,
53+
0,
54+
config.width,
55+
config.height,
56+
)
57+
58+
resolve(canvas)
59+
}
60+
61+
img.onerror = () => {
62+
reject(new Error('Failed to load image'))
63+
}
64+
65+
img.src = dataUrl
66+
},
67+
)
68+
69+
return croppedCanvas.toDataURL('image/jpeg')
70+
}

src/pages/Content/common/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export interface TextSelection {
1111

1212
export interface TranslateJob {
1313
id: string
14-
anchorId: string
1514
text: string
16-
sourceLang?: SupportLanguageKeys
15+
anchorId?: string
16+
sourceLang?: string
1717
}

0 commit comments

Comments
 (0)