java中的作用域

在Java中,我们经常看到public、protected、private这些修饰符。在Java中,这些修饰符可以用来限定访问作用域。

public
定义为public的class、interface可以被其他任何类访问:

package abc;

public class Hello {
    public void hi() {
    }
}

上面的Hello是public,因此,可以被其他包的类访问:

package xyz;

class Main {
    void foo() {
        // Main可以访问Hello
        Hello h = new Hello();
    }
}

定义为public的field、method可以被其他类访问,前提是首先有访问class的权限:

package abc;

public class Hello {
    public void hi() {
    }
}

上面的hi()方法是public,可以被其他类调用,前提是首先要能访问Hello类:

package xyz;

class Main {
    void foo() {
        Hello h = new Hello();
        h.hi();
    }
}

private
定义为private的field、method无法被其他类访问:

package abc;

public class Hello {
    // 不能被其他类调用:
    private void hi() {
    }

    public void hello() {
        this.hi();
    }
}

实际上,确切地说,private访问权限被限定在class的内部,而且与方法声明顺序无关。推荐把private方法放到后面,因为public方法定义了类对外提供的功能,阅读代码的时候,应该先关注public方法:

package abc;

public class Hello {
    public void hello() {
        this.hi();
    }

    private void hi() {
    }
}

由于Java支持嵌套类,如果一个类内部还定义了嵌套类,那么,嵌套类拥有访问private的权限:

// private
public class Main {
    public static void main(String[] args) {
        Inner i = new Inner();
        i.hi();
    }

    // private方法:
    private static void hello() {
        System.out.println("private hello!");
    }

    // 静态内部类:
    static class Inner {
        public void hi() {
            Main.hello();
        }
    }
}

定义在一个class内部的class称为嵌套类(nested class),Java支持好几种嵌套类。

protected
protected作用于继承关系。定义为protected的字段和方法可以被子类访问,以及子类的子类:

package abc;

public class Hello {
    // protected方法:
    protected void hi() {
    }
}

上面的protected方法可以被继承的类访问:

package xyz;

class Main extends Hello {
    void foo() {
        // 可以访问protected方法:
        hi();
    }
}

package
最后,包作用域是指一个类允许访问同一个package的没有public、private修饰的class,以及没有public、protected、private修饰的字段和方法。

package abc;
// package权限的类:
class Hello {
    // package权限的方法:
    void hi() {
    }
}

只要在同一个包,就可以访问package权限的class、field和method:

package abc;

class Main {
    void foo() {
        // 可以访问package权限的类:
        Hello h = new Hello();
        // 可以调用package权限的方法:
        h.hi();
    }
}

注意,包名必须完全一致,包没有父子关系,com.apache和com.apache.abc是不同的包。

局部变量
在方法内部定义的变量称为局部变量,局部变量作用域从变量声明处开始到对应的块结束。方法参数也是局部变量。

package abc;

public class Hello {
    void hi(String name) { // 1
        String s = name.toLowerCase(); // 2
        int len = s.length(); // 3
        if (len < 10) { // 4
            int p = 10 - len; // 5
            for (int i=0; i<10; i++) { // 6
                System.out.println(); // 7
            } // 8
        } // 9
    } // 10
}

我们观察上面的hi()方法代码:

方法参数name是局部变量,它的作用域是整个方法,即1 ~ 10;
变量s的作用域是定义处到方法结束,即2 ~ 10;
变量len的作用域是定义处到方法结束,即3 ~ 10;
变量p的作用域是定义处到if块结束,即5 ~ 9;
变量i的作用域是for循环,即6 ~ 8。
使用局部变量时,应该尽可能把局部变量的作用域缩小,尽可能延后声明局部变量。

final
Java还提供了一个final修饰符。final与访问权限不冲突,它有很多作用。

用final修饰class可以阻止被继承:

package abc;

// 无法被继承:
public final class Hello {
    private int n = 0;
    protected void hi(int t) {
        long i = t;
    }
}

用final修饰method可以阻止被子类覆写:

package abc;

public class Hello {
    // 无法被覆写:
    protected final void hi() {
    }
}

用final修饰field可以阻止被重新赋值:

package abc;

public class Hello {
    private final int n = 0;
    protected void hi() {
        this.n = 1; // error!
    }
}

用final修饰局部变量可以阻止被重新赋值:

package abc;

public class Hello {
    protected void hi(final int t) {
        t = 1; // error!
    }
}

java中包的概念

在现实中,如果小明写了一个Person类,小红也写了一个Person类,现在,小白既想用小明的Person,也想用小红的Person,怎么办?
如果小军写了一个Arrays类,恰好JDK也自带了一个Arrays类,如何解决类名冲突?
在Java中,我们使用package来解决名字冲突。
Java定义了一种名字空间,称之为包:package。一个类总是属于某个包,类名(比如Person)只是一个简写,真正的完整类名是包名.类名。
小明的Person类存放在包ming下面,因此,完整类名是ming.Person;
小红的Person类存放在包hong下面,因此,完整类名是hong.Person;
小军的Arrays类存放在包mr.jun下面,因此,完整类名是mr.jun.Arrays;
JDK的Arrays类存放在包java.util下面,因此,完整类名是java.util.Arrays。
在定义class的时候,我们需要在第一行声明这个class属于哪个包。
小明的Person.java文件:

package ming; // 申明包名ming

public class Person {
}

小军的Arrays.java文件:

package mr.jun; // 申明包名mr.jun

public class Arrays {
}

在Java虚拟机执行的时候,JVM只看完整类名,因此,只要包名不同,类就不同。
包可以是多层结构,用.隔开。例如:java.util。

特别注意

包没有父子关系。java.util和java.util.zip是不同的包,两者没有任何继承关系。
没有定义包名的class,它使用的是默认包,非常容易引起名字冲突,因此,不推荐不写包名的做法。

我们还需要按照包结构把上面的Java文件组织起来。假设以package_sample作为根目录,src作为源码目录,那么所有文件结构就是:

package_sample
└─ src
    ├─ hong
    │  └─ Person.java
    │  ming
    │  └─ Person.java
    └─ mr
       └─ jun
          └─ Arrays.java

即所有Java文件对应的目录层次要和包的层次一致。

编译后的.class文件也需要按照包结构存放。如果使用IDE,把编译后的.class文件放到bin目录下,那么,编译的文件结构就是:

package_sample
└─ bin
   ├─ hong
   │  └─ Person.class
   │  ming
   │  └─ Person.class
   └─ mr
      └─ jun
         └─ Arrays.class

包作用域
位于同一个包的类,可以访问包作用域的字段和方法。不用public、protected、private修饰的字段和方法就是包作用域。例如,Person类定义在hello包下面:

package hello;

public class Person {
    // 包作用域:
    void hello() {
        System.out.println("Hello!");
    }
}

Main类也定义在hello包下面:

package hello;

public class Main {
    public static void main(String[] args) {
        Person p = new Person();
        p.hello(); // 可以调用,因为Main和Person在同一个包
    }
}

import
在一个class中,我们总会引用其他的class。例如,小明的ming.Person类,如果要引用小军的mr.jun.Arrays类,他有三种写法:

第一种,直接写出完整类名,例如:

// Person.java
package ming;

public class Person {
    public void run() {
        // 写完整类名: mr.jun.Arrays
        mr.jun.Arrays arrays = new mr.jun.Arrays();
    }
}

很显然,每次写完整类名比较痛苦。

因此,第二种写法是用import语句,导入小军的Arrays,然后写简单类名:

// Person.java
package ming;

// 导入完整类名:
import mr.jun.Arrays;

public class Person {
    public void run() {
        // 写简单类名: Arrays
        Arrays arrays = new Arrays();
    }
}

在写import的时候,可以使用*,表示把这个包下面的所有class都导入进来(但不包括子包的class):

// Person.java
package ming;

// 导入mr.jun包的所有class:
import mr.jun.*;

public class Person {
    public void run() {
        Arrays arrays = new Arrays();
    }
}

我们一般不推荐这种写法,因为在导入了多个包后,很难看出Arrays类属于哪个包。

还有一种import static的语法,它可以导入一个类的静态字段和静态方法:

package main;

// 导入System类的所有静态字段和静态方法:
import static java.lang.System.*;

public class Main {
    public static void main(String[] args) {
        // 相当于调用System.out.println(…)
        out.println("Hello, world!");
    }
}

import static很少使用。

Java编译器最终编译出的.class文件只使用完整类名,因此,在代码中,当编译器遇到一个class名称时:

如果是完整类名,就直接根据完整类名查找这个class;
如果是简单类名,按下面的顺序依次查找:
查找当前package是否存在这个class;
查找import的包是否包含这个class;
查找java.lang包是否包含这个class。
如果按照上面的规则还无法确定类名,则编译报错。

我们来看一个例子:

// Main.java
package test;

import java.text.Format;

public class Main {
    public static void main(String[] args) {
        java.util.List list; // ok,使用完整类名 -> java.util.List
        Format format = null; // ok,使用import的类 -> java.text.Format
        String s = "hi"; // ok,使用java.lang包的String -> java.lang.String
        System.out.println(s); // ok,使用java.lang包的System -> java.lang.System
        MessageFormat mf = null; // 编译错误:无法找到MessageFormat: MessageFormat cannot be resolved to a type
    }
}

因此,编写class的时候,编译器会自动帮我们做两个import动作:

默认自动import当前package的其他class;
默认自动import java.lang.*。
注意

自动导入的是java.lang包,但类似java.lang.reflect这些包仍需要手动导入。
如果有两个class名称相同,例如,mr.jun.Arrays和java.util.Arrays,那么只能import其中一个,另一个必须写完整类名。

最佳实践
为了避免名字冲突,我们需要确定唯一的包名。推荐的做法是使用倒置的域名来确保唯一性。例如:

org.apache
org.apache.commons.log
com.liaoxuefeng.sample
子包就可以根据功能自行命名。

要注意不要和java.lang包的类重名,即自己的类不要使用这些名字:

String
System
Runtime
...

要注意也不要和JDK常用类重名:

java.util.List
java.text.Format
java.math.BigInteger
...

编译和运行
假设我们创建了如下的目录结构:

work
├── bin
└── src
    └── com
        └── itranswarp
            ├── sample
            │   └── Main.java
            └── world
                └── Person.java

其中,bin目录用于存放编译后的class文件,src目录按包结构存放Java源码,我们怎么一次性编译这些Java源码呢?

首先,确保当前目录是work目录,即存放src和bin的父目录:

$ ls
bin src

然后,编译src目录下的所有Java文件:

$ javac -d ./bin src/**/*.java

命令行-d指定输出的class文件存放bin目录,后面的参数src/*/.java表示src目录下的所有.java文件,包括任意深度的子目录。

注意:Windows不支持**这种搜索全部子目录的做法,所以在Windows下编译必须依次列出所有.java文件:

C:\work> javac -d bin src\com\itranswarp\sample\Main.java src\com\itranswarp\world\Persion.java

使用Windows的PowerShell可以利用Get-ChildItem来列出指定目录下的所有.java文件:

PS C:\work> (Get-ChildItem -Path .\src -Recurse -Filter *.java).FullName
C:\work\src\com\itranswarp\sample\Main.java
C:\work\src\com\itranswarp\world\Person.java

因此,编译命令可写为:

PS C:\work> javac -d .\bin (Get-ChildItem -Path .\src -Recurse -Filter *.java).FullName

如果编译无误,则javac命令没有任何输出。可以在bin目录下看到如下class文件:

bin
└── com
    └── itranswarp
        ├── sample
        │   └── Main.class
        └── world
            └── Person.class

现在,我们就可以直接运行class文件了。根据当前目录的位置确定classpath,例如,当前目录仍为work,则classpath为bin或者./bin:

$ java -cp bin com.itranswarp.sample.Main 
Hello, world!

java面向对象中的静态字段和静态方法

在一个class中定义的字段,我们称之为实例字段。实例字段的特点是,每个实例都有独立的字段,各个实例的同名字段互不影响。

还有一种字段,是用static修饰的字段,称为静态字段:static field。

实例字段在每个实例中都有自己的一个独立“空间”,但是静态字段只有一个共享“空间”,所有实例都会共享该字段。举个例子:

class Person {
    public String name;
    public int age;
    // 定义静态字段number:
    public static int number;
}

我们来看看下面的代码:

// static field
public class Main {
    public static void main(String[] args) {
        Person ming = new Person("Xiao Ming", 12);
        Person hong = new Person("Xiao Hong", 15);
        ming.number = 88;
        System.out.println(hong.number);
        hong.number = 99;
        System.out.println(ming.number);
    }
}

class Person {
    public String name;
    public int age;

    public static int number;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

对于静态字段,无论修改哪个实例的静态字段,效果都是一样的:所有实例的静态字段都被修改了,原因是静态字段并不属于实例
虽然实例可以访问静态字段,但是它们指向的其实都是Person class的静态字段。所以,所有实例共享一个静态字段。

因此,不推荐用实例变量.静态字段去访问静态字段,因为在Java程序中,实例对象并没有静态字段。在代码中,实例对象能访问静态字段只是因为编译器可以根据实例类型自动转换为类名.静态字段来访问静态对象。

推荐用类名来访问静态字段。可以把静态字段理解为描述class本身的字段。对于上面的代码,更好的写法是:

Person.number = 99;
System.out.println(Person.number);

静态方法
有静态字段,就有静态方法。用static修饰的方法称为静态方法。

调用实例方法必须通过一个实例变量,而调用静态方法则不需要实例变量,通过类名就可以调用。静态方法类似其它编程语言的函数。例如:

// static method
public class Main {
    public static void main(String[] args) {
        Person.setNumber(99);
        System.out.println(Person.number);
    }
}

class Person {
    public static int number;

    public static void setNumber(int value) {
        number = value;
    }
}

因为静态方法属于class而不属于实例,因此,静态方法内部,无法访问this变量,也无法访问实例字段,它只能访问静态字段。

通过实例变量也可以调用静态方法,但这只是编译器自动帮我们把实例改写成类名而已。

通常情况下,通过实例变量访问静态字段和静态方法,会得到一个编译警告。

静态方法经常用于工具类。例如:

Arrays.sort()
Math.random()

静态方法也经常用于辅助方法。注意到Java程序的入口main()也是静态方法。

接口的静态字段
因为interface是一个纯抽象类,所以它不能定义实例字段。但是,interface是可以有静态字段的,并且静态字段必须为final类型:

public interface Person {
    public static final int MALE = 1;
    public static final int FEMALE = 2;
}

实际上,因为interface的字段只能是public static final类型,所以我们可以把这些修饰符都去掉,上述代码可以简写为:

public interface Person {
    // 编译器会自动加上public static final:
    int MALE = 1;
    int FEMALE = 2;
}

编译器会自动把该字段变为public static final类型。

java面向对象中的接口概念

特性 接口 (Interface) 抽象类 (Abstract Class)
继承方式 可以被多个类实现(多重继承) 只能被一个类继承(单继承)
方法实现 默认方法没有实现,除非是 defaultstatic 方法 可以有抽象方法,也可以有具体方法
成员变量 默认是 public static final 可以是任何类型的变量
构造方法 无构造方法 可以有构造方法
访问修饰符 只能是 public 可以是 publicprotectedprivate
是否支持多重继承 支持多重继承(可以实现多个接口) 不支持多重继承(单继承)
适用场景 用于定义行为规范,多个不相关的类共享相同的行为 用于提供共性功能,提供默认实现,通常用于类层次结构

在抽象类中,抽象方法本质上是定义接口规范:即规定高层类的接口,从而保证所有子类都有相同的接口实现,这样,多态就能发挥出威力。

如果一个抽象类没有字段,所有方法全部都是抽象方法:

abstract class Person {
    public abstract void run();
    public abstract String getName();
}

就可以把该抽象类改写为接口:interface。

在Java中,使用interface可以声明一个接口:

interface Person {
    void run();
    String getName();
}

所谓interface,就是比抽象类还要抽象的纯抽象接口,因为它连字段都不能有。因为接口定义的所有方法默认都是public abstract的,所以这两个修饰符不需要写出来(写不写效果都一样)。
当一个具体的class去实现一个interface时,需要使用implements关键字。举个例子:

class Student implements Person {
    private String name;

    public Student(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println(this.name + " run");
    }

    @Override
    public String getName() {
        return this.name;
    }
}

我们知道,在Java中,一个类只能继承自另一个类,不能从多个类继承。但是,一个类可以实现多个interface,例如:

class Student implements Person, Hello { // 实现了两个interface
    ...
}

接口继承

一个interface可以继承自另一个interface。interface继承自interface使用extends,它相当于扩展了接口的方法。例如:

interface Hello {
    void hello();
}

interface Person extends Hello {
    void run();
    String getName();
}

此时,Person接口继承自Hello接口,因此,Person接口现在实际上有3个抽象方法签名,其中一个来自继承的Hello接口。

default方法

在接口中,可以定义default方法。例如,把Person接口的run()方法改为default方法:

// interface
public class Main {
    public static void main(String[] args) {
        Person p = new Student("Xiao Ming");
        p.run();
    }
}

interface Person {
    String getName();
    default void run() {
        System.out.println(getName() + " run");
    }
}

class Student implements Person {
    private String name;

    public Student(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
}

实现类可以不必覆写default方法。default方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。

default方法和抽象类的普通方法是有所不同的。因为interface没有字段,default方法无法访问字段,而抽象类的普通方法可以访问实例字段。

微信内置ocr私有部署

本文主要在linux使用
系统为Debian12
注意 在云服务器部署需要测试服务器是否可以使用,我在阿里云上测试 与本地效果差别特别大
别人封装好的docker镜像

docker pull golangboyme/wxocr:latest
docker run -d -p 5000:5000 --name wechat-ocr-api golangboyme/wxocr

依赖项目
https://github.com/swigger/wechat-ocr/tree/master/src
编译的时候需要用高本版的python 有一个最低版本我忘记了 用3.12以上肯定没问题

mkdir build
cd build 
cmake..
make

得到一个python的模块
其他相关的文件可以从微信的linux安装包找到

python调用代码

import wcocr
import os
import uuid
import base64
from flask import Flask, request, jsonify, render_template, send_from_directory

app = Flask(__name__)
wcocr.init("./wx/opt/wechat/wxocr", "./wx/opt/wechat")

@app.route("/ocr", methods=["POST"])
def ocr():
    try:
        # Get base64 image from request
        image_data = request.json.get("image")
        if not image_data:
            return jsonify({"error": "No image data provided"}), 400
        # Extract image type from base64 data
        image_type, base64_data = extract_image_type(image_data)
        if not image_type:
            return jsonify({"error": "Invalid base64 image data"}), 400

        # Create temp directory if not exists
        temp_dir = "temp"
        if not os.path.exists(temp_dir):
            os.makedirs(temp_dir)

        # Generate unique filename and save image
        filename = os.path.join(temp_dir, f"{str(uuid.uuid4())}.{image_type}")
        try:
            image_bytes = base64.b64decode(base64_data)
            with open(filename, "wb") as f:
                f.write(image_bytes)

            # Process image with OCR
            result = wcocr.ocr(filename)
            return jsonify({"result": result})

        finally:
            # Clean up temp file
            if os.path.exists(filename):
                os.remove(filename)

    except Exception as e:
        return jsonify({"error": str(e)}), 500

# 创建静态文件夹
static_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
if not os.path.exists(static_dir):
    os.makedirs(static_dir)

def extract_image_type(base64_data):
    # Check if the base64 data has the expected prefix
    if base64_data.startswith("data:image/"):
        # Extract the image type from the prefix
        prefix_end = base64_data.find(";base64,")
        if prefix_end != -1:
            return (
                base64_data[len("data:image/") : prefix_end],
                base64_data.split(";base64,")[-1],
            )
    return "png", base64_data

@app.route("/")
def index():
    return render_template("index.html")

# Handle unsupported methods for /ocr route
@app.route("/ocr", methods=["GET", "PUT", "DELETE", "PATCH"])
def unsupported_method():
    return jsonify({"error": "Method not allowed"}), 405

# Handle non-existent paths
@app.errorhandler(404)
def not_found(e):
    return jsonify({"error": "Resource not found"}), 404

if __name__ == "__main__":
    # 确保templates目录存在
    templates_dir = os.path.join(
        os.path.dirname(os.path.abspath(__file__)), "templates"
    )
    if not os.path.exists(templates_dir):
        os.makedirs(templates_dir)

    # 确保temp目录存在
    temp_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "temp")
    if not os.path.exists(temp_dir):
        os.makedirs(temp_dir)

    app.run(host="0.0.0.0", port=5000, threaded=True)

html文件

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>微信OCR文字识别工具</title>
    <style>
        :root {
            --primary-color: #07c160;
            --secondary-color: #576b95;
        }

        body {
            font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
            max-width: 1200px;
            margin: 0 auto;
            padding: 20px;
            background: #f5f5f5;
            color: #333;
        }

        .container {
            background: white;
            border-radius: 12px;
            padding: 30px;
            box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
        }

        h1 {
            color: var(--primary-color);
            text-align: center;
            margin-bottom: 30px;
        }

        .upload-section {
            border: 2px dashed #ddd;
            border-radius: 8px;
            padding: 30px;
            text-align: center;
            margin: 20px 0;
            transition: all 0.3s;
        }

        .upload-section:hover {
            border-color: var(--primary-color);
            background: #f8fff9;
        }

        .preview-image {
            max-width: 100%;
            max-height: 400px;
            margin: 20px 0;
            border-radius: 8px;
            display: none;
        }

        .input-group {
            margin: 20px 0;
        }

        input[type="file"],
        input[type="text"] {
            width: 100%;
            padding: 12px;
            border: 1px solid #ddd;
            border-radius: 6px;
            margin: 10px 0;
        }

        button {
            background: var(--primary-color);
            color: white;
            border: none;
            padding: 12px 30px;
            border-radius: 6px;
            cursor: pointer;
            font-size: 16px;
            transition: all 0.3s;
        }

        button:hover {
            opacity: 0.9;
            transform: translateY(-1px);
        }

        .result-table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 20px;
            display: none;
        }

        .result-table th,
        .result-table td {
            padding: 12px;
            border: 1px solid #eee;
            text-align: left;
        }

        .result-table th {
            background: var(--secondary-color);
            color: white;
        }

        .loading {
            display: none;
            text-align: center;
            color: var(--primary-color);
            margin: 20px 0;
        }

        .error {
            color: #ff4d4f;
            margin: 10px 0;
            display: none;
        }

        .image-container {
            position: relative;
            margin: 20px 0;
            display: inline-block;
        }

        .text-box {
            position: absolute;
            border: 2px solid var(--primary-color);
            background-color: rgba(7, 193, 96, 0.1);
            cursor: pointer;
        }

        .text-tooltip {
            position: absolute;
            background: white;
            border: 1px solid #ddd;
            padding: 8px;
            border-radius: 4px;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
            z-index: 100;
            display: none;
            max-width: 300px;
            word-break: break-word;
        }

        .confidence {
            color: var(--primary-color);
            font-weight: bold;
        }
    </style>
</head>

<body>
    <div class="container">
        <h1>微信OCR文字识别工具</h1>

        <!-- 上传区域 -->
        <div class="upload-section">
            <div class="input-group">
                <input type="file" id="fileInput" accept="image/*">
                <p>或拖拽图片到此区域</p>
                <input type="text" id="urlInput" placeholder="输入图片URL地址">
            </div>
            <button onclick="processImage()">开始识别</button>
        </div>

        <!-- 图片预览 -->
        <div class="image-container" id="imageContainer">
            <img id="preview" class="preview-image">
            <!-- 文本框将在这里动态添加 -->
        </div>

        <!-- 加载状态 -->
        <div class="loading" id="loading">识别中,请稍候...</div>

        <!-- 错误提示 -->
        <div class="error" id="error"></div>

        <!-- 结果显示 -->
        <table class="result-table" id="resultTable">
            <thead>
                <tr>
                    <th>文本内容</th>
                    <th>置信度</th>
                    <th>位置信息 (左, 上, 右, 下)</th>
                </tr>
            </thead>
            <tbody id="resultBody"></tbody>
        </table>

        <!-- 使用说明 -->
        <h2>API接口说明</h2>
        <h3>请求方式</h3>
        <pre>POST /ocr</pre>

        <h3>请求示例</h3>
        <pre>
{
  "image": "BASE64_ENCODED_IMAGE_DATA"
}</pre>

        <h3>返回示例</h3>
        <pre id="responseSample"></pre>
    </div>

    <script>
        // 默认的API地址
        const API_ENDPOINT = window.location.origin + '/ocr';

        // 初始化拖放功能
        const uploadSection = document.querySelector('.upload-section');
        uploadSection.addEventListener('dragover', (e) => {
            e.preventDefault();
            uploadSection.style.backgroundColor = '#f0fff0';
        });

        uploadSection.addEventListener('drop', (e) => {
            e.preventDefault();
            uploadSection.style.backgroundColor = '';
            const file = e.dataTransfer.files[0];
            handleFile(file);
        });

        // 处理文件选择
        document.getElementById('fileInput').addEventListener('change', function (e) {
            handleFile(e.target.files[0]);
        });

        // 处理文件上传
        async function handleFile(file) {
            if (!file) return;
            if (!file.type.startsWith('image/')) {
                showError('请上传图片文件');
                return;
            }

            // 显示预览图片
            const reader = new FileReader();
            reader.onload = (e) => {
                document.getElementById('preview').src = e.target.result;
                document.getElementById('preview').style.display = 'block';

                // 清除之前的文本框
                const imageContainer = document.getElementById('imageContainer');
                const existingBoxes = imageContainer.querySelectorAll('.text-box, .text-tooltip');
                existingBoxes.forEach(box => box.remove());
            };
            reader.readAsDataURL(file);
        }

        // 开始处理图像
        async function processImage() {
            const file = document.getElementById('fileInput').files[0];
            const url = document.getElementById('urlInput').value;
            let base64Data = '';

            try {
                showLoading();
                clearError();

                if (file) {
                    base64Data = await fileToBase64(file);
                } else if (url) {
                    base64Data = await urlToBase64(url);
                } else {
                    showError('请选择图片或输入图片URL');
                    return;
                }

                // 发送请求
                const response = await fetch(API_ENDPOINT, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({ image: base64Data })
                });

                const data = await response.json();
                handleResponse(data);
            } catch (error) {
                showError(`请求失败:${error.message}`);
            } finally {
                hideLoading();
            }
        }

        // 处理响应数据
        function handleResponse(data) {
            // 处理新的响应结构
            const resultData = data.result || data;

            if (resultData.errcode !== 0) {
                showError(`识别失败,错误码:${resultData.errcode}`);
                return;
            }

            // 显示结果表格
            const tbody = document.getElementById('resultBody');
            tbody.innerHTML = '';
            resultData.ocr_response.forEach(item => {
                const row = document.createElement('tr');
                row.innerHTML = `
                    <td>${item.text}</td>
                    <td>${(item.rate * 100).toFixed(2)}%</td>
                    <td>(${item.left.toFixed(1)}, ${item.top.toFixed(1)}, 
                        ${item.right.toFixed(1)}, ${item.bottom.toFixed(1)})</td>
                `;
                tbody.appendChild(row);
            });
            document.getElementById('resultTable').style.display = 'table';

            // 在图片上绘制识别区域
            drawTextBoxes(resultData.ocr_response, resultData.width, resultData.height);
        }

        // 在图片上绘制文本框
        function drawTextBoxes(ocrResults, originalWidth, originalHeight) {
            const imageContainer = document.getElementById('imageContainer');
            const preview = document.getElementById('preview');

            // 清除之前的文本框
            const existingBoxes = imageContainer.querySelectorAll('.text-box, .text-tooltip');
            existingBoxes.forEach(box => box.remove());

            // 获取图片的实际显示尺寸和位置
            const imgRect = preview.getBoundingClientRect();
            const containerRect = imageContainer.getBoundingClientRect();

            // 计算图片相对于容器的偏移
            const offsetX = imgRect.left - containerRect.left;
            const offsetY = imgRect.top - containerRect.top;

            // 计算缩放比例
            const scaleX = imgRect.width / originalWidth;
            const scaleY = imgRect.height / originalHeight;

            // 为每个识别结果创建文本框
            ocrResults.forEach((item, index) => {
                // 创建文本框
                const textBox = document.createElement('div');
                textBox.className = 'text-box';

                // 精确定位文本框,考虑图片在容器中的偏移
                const left = item.left * scaleX + offsetX;
                const top = item.top * scaleY + offsetY;
                const width = (item.right - item.left) * scaleX;
                const height = (item.bottom - item.top) * scaleY;

                // 设置文本框位置和大小
                textBox.style.left = `${left}px`;
                textBox.style.top = `${top}px`;
                textBox.style.width = `${width}px`;
                textBox.style.height = `${height}px`;
                textBox.dataset.index = index;

                // 创建提示框
                const tooltip = document.createElement('div');
                tooltip.className = 'text-tooltip';
                tooltip.innerHTML = `
                    <div>${item.text}</div>
                    <div class="confidence">置信度: ${(item.rate * 100).toFixed(2)}%</div>
                `;

                // 添加鼠标事件
                textBox.addEventListener('mouseenter', function (e) {
                    tooltip.style.left = `${e.pageX - imageContainer.offsetLeft + 10}px`;
                    tooltip.style.top = `${e.pageY - imageContainer.offsetTop + 10}px`;
                    tooltip.style.display = 'block';
                });

                textBox.addEventListener('mousemove', function (e) {
                    tooltip.style.left = `${e.pageX - imageContainer.offsetLeft + 10}px`;
                    tooltip.style.top = `${e.pageY - imageContainer.offsetTop + 10}px`;
                });

                textBox.addEventListener('mouseleave', function () {
                    tooltip.style.display = 'none';
                });

                imageContainer.appendChild(textBox);
                imageContainer.appendChild(tooltip);
            });
        }

        // 工具函数
        function fileToBase64(file) {
            return new Promise((resolve, reject) => {
                const reader = new FileReader();
                reader.onload = () => resolve(reader.result);
                reader.onerror = error => reject(error);
                reader.readAsDataURL(file);
            });
        }

        async function urlToBase64(url) {
            const response = await fetch(url);
            const blob = await response.blob();
            return fileToBase64(blob);
        }

        function showLoading() {
            document.getElementById('loading').style.display = 'block';
        }

        function hideLoading() {
            document.getElementById('loading').style.display = 'none';
        }

        function showError(message) {
            const errorDiv = document.getElementById('error');
            errorDiv.textContent = message;
            errorDiv.style.display = 'block';
        }

        function clearError() {
            document.getElementById('error').style.display = 'none';
        }

        // 初始化示例响应显示
        document.getElementById('responseSample').textContent = JSON.stringify({
            "result": {
                "errcode": 0,
                "height": 258,
                "imgpath": "temp/0cfbda36-a05d-4cba-9a72-cec6833d305d.png",
                "ocr_response": [
                    {
                        "bottom": 41.64999771118164,
                        "left": 33.6875,
                        "rate": 0.9951504468917847,
                        "right": 164.76248168945312,
                        "text": "API接口说明",
                        "top": 18.98750114440918
                    }
                ],
                "width": 392
            }
        }, null, 2);
    </script>
</body>

</html>
1 2 3 11