├── .eslintrc.cjs
├── .gitignore
├── .prettierignore
├── .prettierrc
├── README.md
├── index.html
├── package.json
├── postcss.config.cjs
├── public
└── logo.svg
├── src
├── Layout
│ ├── Sidebar.tsx
│ └── index.tsx
├── assets
│ └── svg
│ │ ├── delete.svg
│ │ ├── down.svg
│ │ ├── download.svg
│ │ ├── full_screen.svg
│ │ ├── lock.svg
│ │ ├── menu.svg
│ │ ├── plus.svg
│ │ ├── recover.svg
│ │ ├── right_arrow.svg
│ │ ├── ruler.svg
│ │ └── unlock.svg
├── components
│ ├── Button
│ │ ├── constant.ts
│ │ └── index.tsx
│ ├── CardSelect
│ │ └── index.tsx
│ ├── CustomModal
│ │ ├── index.css
│ │ └── index.tsx
│ ├── ImageCrop
│ │ ├── canvasPreview.ts
│ │ ├── constant.ts
│ │ └── index.tsx
│ ├── List
│ │ └── index.tsx
│ ├── ProgressBar
│ │ ├── constant.ts
│ │ └── index.tsx
│ ├── Ruler
│ │ └── index.tsx
│ ├── Table
│ │ ├── index.tsx
│ │ └── type.ts
│ ├── Thumbnail
│ │ └── index.tsx
│ ├── Tip
│ │ └── index.tsx
│ └── Upload
│ │ ├── index.tsx
│ │ └── type.ts
├── index.css
├── main.tsx
├── pages
│ ├── encryption
│ │ ├── ControlPanel
│ │ │ ├── constant.ts
│ │ │ ├── index.tsx
│ │ │ └── type.ts
│ │ ├── Output
│ │ │ ├── constant.ts
│ │ │ └── index.tsx
│ │ └── index.tsx
│ └── steganography
│ │ ├── ControlPanel
│ │ ├── constant.ts
│ │ ├── index.tsx
│ │ └── type.ts
│ │ ├── Output
│ │ ├── constant.ts
│ │ └── index.tsx
│ │ └── index.tsx
├── plugins
│ ├── encryption
│ │ ├── Arnold
│ │ │ ├── index.json
│ │ │ └── index.ts
│ │ ├── DNA
│ │ │ ├── index.json
│ │ │ └── index.ts
│ │ ├── Logistic
│ │ │ ├── index.json
│ │ │ └── index.ts
│ │ └── Tent
│ │ │ ├── index.json
│ │ │ └── index.ts
│ └── steganography
│ │ ├── DCT
│ │ ├── dct.ts
│ │ ├── index.json
│ │ ├── index.ts
│ │ ├── mersenne-twister.d.ts
│ │ ├── mersenne-twister.js
│ │ └── utf_8.ts
│ │ └── lsb
│ │ ├── index.json
│ │ ├── index.ts
│ │ ├── lsb.ts
│ │ ├── mersenne-twister.d.ts
│ │ ├── mersenne-twister.js
│ │ └── utf_8.ts
├── routes.tsx
├── service
│ ├── cache
│ │ └── index.ts
│ ├── image
│ │ ├── index.ts
│ │ ├── type.ts
│ │ └── worker.ts
│ ├── plugin
│ │ ├── index.ts
│ │ └── type.ts
│ └── worker
│ │ ├── index.ts
│ │ └── type.ts
├── utils
│ ├── dna.ts
│ ├── file.ts
│ ├── function.ts
│ ├── index.ts
│ ├── number.ts
│ ├── object.ts
│ ├── string.ts
│ ├── webgl.ts
│ └── zip.ts
└── vite-env.d.ts
├── tailwind.config.cjs
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.ts
└── yarn.lock
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2021: true,
5 | node: true,
6 | },
7 | extends: [
8 | "eslint:recommended",
9 | "plugin:react/recommended",
10 | "plugin:@typescript-eslint/recommended",
11 | "plugin:react-hooks/recommended",
12 | "plugin:prettier/recommended",
13 | ],
14 | overrides: [],
15 | parser: "@typescript-eslint/parser",
16 | parserOptions: {
17 | ecmaVersion: "latest",
18 | sourceType: "module",
19 | },
20 | plugins: ["react", "@typescript-eslint", "react-hooks", "prettier"], //声明插件,不声明也行
21 | rules: {
22 | "@typescript-eslint/no-explicit-any": "off",
23 | "@typescript-eslint/no-non-null-assertion": "off",
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | build/
3 | dist/
4 | *.log
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "endOfLine": "auto"
3 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 图像加密系统-毕业设计
2 |
3 | ## 技术说明
4 |
5 | - Vite
6 | - Typescript
7 | - React
8 | - Tailwind
9 |
10 | ## DEMO
11 | https://encry-image.tempest.blue/encryption
12 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 图像加密系统
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "image-encryption-system",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "start": "vite --host 0.0.0.0",
9 | "build": "tsc && vite build",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@types/crypto-js": "^4.1.1",
14 | "crypto-js": "^4.1.1",
15 | "file-saver": "^2.0.5",
16 | "flowbite": "^1.6.3",
17 | "gl-matrix": "^3.4.3",
18 | "immer": "^9.0.19",
19 | "jszip": "^3.10.1",
20 | "p-limit": "^4.0.0",
21 | "pica": "^9.0.1",
22 | "react": "^18.2.0",
23 | "react-dom": "^18.2.0",
24 | "react-dropzone": "^14.2.3",
25 | "react-image-crop": "^10.0.9",
26 | "react-modal": "^3.16.1",
27 | "react-router-dom": "^6.8.1",
28 | "spark-md5": "^3.0.2"
29 | },
30 | "devDependencies": {
31 | "@types/file-saver": "^2.0.5",
32 | "@types/node": "^18.14.0",
33 | "@types/pica": "^9.0.1",
34 | "@types/react": "^18.0.27",
35 | "@types/react-dom": "^18.0.10",
36 | "@types/react-modal": "^3.13.1",
37 | "@types/spark-md5": "^3.0.2",
38 | "@typescript-eslint/eslint-plugin": "^5.54.0",
39 | "@typescript-eslint/parser": "^5.54.0",
40 | "@vitejs/plugin-react": "^3.1.0",
41 | "autoprefixer": "^10.4.13",
42 | "eslint": "^8.35.0",
43 | "eslint-config-prettier": "^8.6.0",
44 | "eslint-plugin-prettier": "^4.2.1",
45 | "eslint-plugin-react": "^7.32.2",
46 | "eslint-plugin-react-hooks": "^4.6.0",
47 | "postcss": "^8.4.21",
48 | "prettier": "^2.8.4",
49 | "tailwindcss": "^3.2.7",
50 | "typescript": "^4.9.3",
51 | "vite": "^4.1.0",
52 | "vite-plugin-svgr": "^2.4.0"
53 | }
54 | }
--------------------------------------------------------------------------------
/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/Layout/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment, useState } from "react";
2 | import { NavLink } from "react-router-dom";
3 | import routes, { CustomRouteObject } from "@/routes";
4 | import { ReactComponent as SVG_down } from "@/assets/svg/down.svg";
5 | import { ReactComponent as SVG_menu } from "@/assets/svg/menu.svg";
6 |
7 | const MenuNavLink: React.FC<{ route: CustomRouteObject; zIndex?: number }> = ({
8 | route,
9 | zIndex = 0,
10 | }) => {
11 | const hasChildren = route.children && route.children.length > 0;
12 | const [expanded, setExpanded] = useState(false);
13 |
14 | const handleClick = (event: React.MouseEvent) => {
15 | if (hasChildren) {
16 | event.preventDefault();
17 | }
18 | setExpanded(!expanded);
19 | };
20 |
21 | return (
22 |
23 |
28 | `flex items-center w-full px-4 py-2 text-gray-700 hover:bg-gray-200 hover:text-gray-900 rounded-md ${
29 | hasChildren ? "cursor-pointer" : ""
30 | } ${isActive ? "!bg-white shadow-md" : ""}`
31 | }
32 | >
33 |
34 | {route.name}
35 |
36 | {hasChildren && (
37 |
42 | )}
43 |
44 | {hasChildren && (
45 |
50 | {route.children?.map((childRoute: CustomRouteObject, index) => (
51 |
52 | ))}
53 |
54 | )}
55 |
56 | );
57 | };
58 | const MenuNavLink2: React.FC<{ route: CustomRouteObject; zIndex?: number }> = ({
59 | route,
60 | zIndex = 0,
61 | }) => {
62 | const hasChildren = route.children && route.children.length > 0;
63 | const [expanded, setExpanded] = useState(false);
64 |
65 | const handleClick = (event: React.MouseEvent) => {
66 | if (hasChildren) {
67 | event.preventDefault();
68 | }
69 | setExpanded(!expanded);
70 | };
71 |
72 | return (
73 |
74 |
79 | `inline-block w-full px-4 py-2 text-gray-700 hover:text-gray-900 rounded-lg ${
80 | hasChildren ? "cursor-pointer" : ""
81 | } ${isActive ? "!bg-white shadow-md" : ""}`
82 | }
83 | >
84 |
85 |
{route.name}
86 |
87 | {route.description}
88 |
89 |
90 | {hasChildren && (
91 |
96 | )}
97 |
98 | {hasChildren && (
99 |
104 | {route.children?.map((childRoute: CustomRouteObject, index) => (
105 |
106 | ))}
107 |
108 | )}
109 |
110 | );
111 | };
112 |
113 | const Sidebar: React.FC = () => {
114 | return (
115 |
116 |
125 |
126 |
131 |
132 |
133 | 图像加密系统
134 |
135 |
144 |
145 |
146 |
147 | {routes?.map(
148 | (route, index) =>
149 | route.name &&
150 | )}
151 |
152 |
153 |
154 |
155 | );
156 | };
157 |
158 | export default Sidebar;
159 |
--------------------------------------------------------------------------------
/src/Layout/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Tip } from "@/components/Tip";
3 | import { isChromiumBased } from "@/utils";
4 | import Sidebar from "./Sidebar";
5 |
6 | export default function Layout({ children }: { children?: any }) {
7 | return (
8 |
9 |
10 |
11 |
12 | {children}
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/assets/svg/delete.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/down.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/download.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/full_screen.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/lock.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/menu.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/plus.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/recover.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/right_arrow.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/ruler.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/unlock.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/Button/constant.ts:
--------------------------------------------------------------------------------
1 | export const TYPES = {
2 | white:
3 | "text-gray-900 bg-white border border-gray-300 hover:bg-gray-100 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600",
4 | blue: "text-white bg-blue-700 hover:bg-blue-800 dark:bg-blue-600 dark:hover:bg-blue-700",
5 | };
6 |
--------------------------------------------------------------------------------
/src/components/Button/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { TYPES } from "./constant";
3 |
4 | export default function Button(
5 | props: React.DetailedHTMLProps<
6 | React.ButtonHTMLAttributes,
7 | HTMLButtonElement
8 | > & { className?: string; typeColor?: "white" | "blue"; disabled?: boolean }
9 | ) {
10 | const { typeColor = "blue", className = "", disabled, ...rest } = props;
11 | const disabledClass = disabled ? "opacity-70 pointer-events-none" : "";
12 | return (
13 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/CardSelect/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ReactComponent as SVG_right_arrow } from "@/assets/svg/right_arrow.svg";
3 | interface CardSelectOptionType {
4 | title: string;
5 | description: string;
6 | }
7 |
8 | export default function CardSelect({
9 | options,
10 | disabled,
11 | onChange,
12 | className,
13 | }: {
14 | options: CardSelectOptionType[];
15 | disabled?: boolean;
16 | className?: string;
17 | onChange?: (value: number) => void;
18 | }) {
19 | //选择回调
20 | const handleOptionChange = (event: React.ChangeEvent) => {
21 | const value = Number(event.target.value);
22 | onChange?.(value);
23 | };
24 | const disabledClass = disabled ? "opacity-80 pointer-events-none" : "";
25 | return (
26 |
27 | {options.map((item, index) => (
28 | -
29 |
40 |
50 |
51 | ))}
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/CustomModal/index.css:
--------------------------------------------------------------------------------
1 | .ReactModalPortal .ReactModal__Overlay {
2 | opacity: 0;
3 | transition: opacity 250ms ease-in-out;
4 | }
5 | .ReactModalPortal .ReactModal__Overlay--after-open {
6 | opacity: 1;
7 | }
8 |
9 | .ReactModalPortal .ReactModal__Overlay--before-close {
10 | opacity: 0;
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/CustomModal/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import Modal from "react-modal";
3 | import "./index.css";
4 | Modal.setAppElement("#root");
5 |
6 | interface CustomModalProps {
7 | isOpen: boolean;
8 | children?: React.ReactNode;
9 | className?: string;
10 | }
11 |
12 | const CustomModal: React.FC = ({
13 | isOpen,
14 | children,
15 | className,
16 | }) => {
17 | const [isDestroy, setIsDestroy] = useState(false);
18 | //模态框打开后创建元素
19 | const handleAfterOpen = () => {
20 | setIsDestroy(false);
21 | };
22 | //模态框关闭后销毁内部元素
23 | const handleAfterClose = () => {
24 | setIsDestroy(true);
25 | };
26 |
27 | return (
28 |
37 | {isDestroy ? null : children}
38 |
39 | );
40 | };
41 |
42 | export default CustomModal;
43 |
--------------------------------------------------------------------------------
/src/components/ImageCrop/canvasPreview.ts:
--------------------------------------------------------------------------------
1 | import { file2Image } from "@/utils/file";
2 | import { PixelCrop } from "react-image-crop";
3 |
4 | const TO_RADIANS = Math.PI / 180;
5 |
6 | export async function canvasPreview(
7 | imageFile: File,
8 | imageView: HTMLImageElement,
9 | crop: PixelCrop,
10 | scale = 1,
11 | rotate = 0
12 | ) {
13 | const image = await file2Image(imageFile);
14 | const scaleX = image.naturalWidth / imageView.width;
15 | const scaleY = image.naturalHeight / imageView.height;
16 | // devicePixelRatio slightly increases sharpness on retina devices
17 | // at the expense of slightly slower render times and needing to
18 | // size the image back down if you want to download/upload and be
19 | // true to the images natural size.
20 | const pixelRatio = window.devicePixelRatio;
21 | // const pixelRatio = 1
22 |
23 | const canvasWidth = Math.floor(crop.width * scaleX * pixelRatio);
24 | const canvasHeight = Math.floor(crop.height * scaleY * pixelRatio);
25 | const offscreenCanvas = new OffscreenCanvas(canvasWidth, canvasHeight);
26 |
27 | const ctx = offscreenCanvas.getContext(
28 | "2d"
29 | ) as OffscreenCanvasRenderingContext2D;
30 |
31 | if (!ctx) {
32 | throw new Error("No 2d context");
33 | }
34 |
35 | ctx.scale(pixelRatio, pixelRatio);
36 |
37 | ctx.imageSmoothingQuality = "high";
38 |
39 | const cropX = crop.x * scaleX;
40 | const cropY = crop.y * scaleY;
41 |
42 | const rotateRads = rotate * TO_RADIANS;
43 | const centerX = image.naturalWidth / 2;
44 | const centerY = image.naturalHeight / 2;
45 |
46 | //ctx.save();
47 | //debugger;
48 | // 5) Move the crop origin to the canvas origin (0,0)
49 | ctx.translate(-cropX, -cropY);
50 | // 4) Move the origin to the center of the original position
51 | ctx.translate(centerX, centerY);
52 | // 3) Rotate around the origin
53 | ctx.rotate(rotateRads);
54 | // 2) Scale the image
55 | ctx.scale(scale, scale);
56 | // 1) Move the center of the image to the origin (0,0)
57 | ctx.translate(-centerX, -centerY);
58 |
59 | ctx.drawImage(
60 | image,
61 | 0,
62 | 0,
63 | image.naturalWidth,
64 | image.naturalHeight,
65 | 0,
66 | 0,
67 | image.naturalWidth,
68 | image.naturalHeight
69 | );
70 |
71 | // ctx.restore();
72 | return offscreenCanvas;
73 | }
74 |
--------------------------------------------------------------------------------
/src/components/ImageCrop/constant.ts:
--------------------------------------------------------------------------------
1 | export const DEFAULT_SCALE = {
2 | min: 0,
3 | max: 100,
4 | defaultValue: 0,
5 | suffix: "%",
6 | };
7 | export const DEFAULT_ROTATE = {
8 | min: -180,
9 | max: 180,
10 | defaultValue: 0,
11 | suffix: "°",
12 | };
13 |
--------------------------------------------------------------------------------
/src/components/ImageCrop/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useState } from "react";
2 | import CustomModal from "@/components/CustomModal";
3 | import ImageCrop, {
4 | centerCrop,
5 | Crop,
6 | makeAspectCrop,
7 | PixelCrop,
8 | } from "react-image-crop";
9 | import "react-image-crop/dist/ReactCrop.css";
10 | import { ReactComponent as SVG_lock } from "@/assets/svg/lock.svg";
11 | import { ReactComponent as SVG_unlock } from "@/assets/svg/unlock.svg";
12 | import { ReactComponent as SVG_full_screen } from "@/assets/svg/full_screen.svg";
13 | import { ReactComponent as SVG_recover } from "@/assets/svg/recover.svg";
14 |
15 | import Ruler from "../Ruler";
16 | import { FileType } from "../Upload/type";
17 | import { canvasPreview } from "./canvasPreview";
18 | import { DEFAULT_ROTATE, DEFAULT_SCALE } from "./constant";
19 |
20 | interface ModalContentProps {
21 | imageFile: FileType | null;
22 | onClose?: () => void;
23 | onChange?: (cropFile: File, originMD5: string) => void;
24 | }
25 | interface ImageCropModalProps {
26 | imageFile: FileType | null;
27 | isOpen: boolean;
28 | onClose?: () => void;
29 | onChange?: (cropFile: File, originMD5: string) => void;
30 | }
31 |
32 | const ModalContent: React.FC = ({
33 | imageFile,
34 | onClose,
35 | onChange,
36 | }) => {
37 | //裁剪范围
38 | const [crop, setCrop] = useState();
39 | //裁剪完成后的Crop
40 | const [completedCrop, setCompletedCrop] = useState();
41 | //裁剪比例
42 | const [aspect, setAspect] = useState(undefined);
43 | //缩放比例
44 | const [scale, setScale] = useState(1);
45 | //旋转角度
46 | const [rotate, setRotate] = useState(0);
47 | //强制更改刻度(可用于重置刻度值)
48 | const [focusValue, setFocusValue] = useState({
49 | scale: 0,
50 | rotate: 0,
51 | });
52 | //图片引用
53 | const imageRef = React.useRef(null);
54 | /**
55 | *
56 | * @param newCrop 新的裁剪范围
57 | */
58 | const handleCropChange = (newCrop: Crop) => {
59 | setCrop(newCrop);
60 | };
61 | /**
62 | *
63 | * @param crop 裁剪完成后的Crop
64 | */
65 | const handleCropComplete = (crop: PixelCrop) => {
66 | setCompletedCrop(crop);
67 | };
68 | /**
69 | * 刻度尺组件回调
70 | */
71 | const handleRulerChange = useCallback(
72 | (values: { scale: number; rotate: number }) => {
73 | setScale(values.scale);
74 | setRotate(values.rotate);
75 | },
76 | []
77 | );
78 | /**
79 | * 确认裁剪
80 | */
81 | const handleConfirm = async () => {
82 | if (!imageFile || !imageRef.current || !completedCrop) return;
83 | //如果没有裁剪范围则取消裁剪
84 | const { width, height } = imageRef.current;
85 | if (
86 | !completedCrop ||
87 | completedCrop.width === 0 ||
88 | completedCrop.height === 0 ||
89 | (completedCrop.width === width &&
90 | completedCrop.height === height &&
91 | scale === 1 &&
92 | rotate === 0)
93 | ) {
94 | handleCancel();
95 | return;
96 | }
97 | //获取绘制完成的canvas
98 | const offscreenCanvas = await canvasPreview(
99 | imageFile.file,
100 | imageRef.current,
101 | completedCrop,
102 | scale,
103 | rotate
104 | );
105 | const blob = await (offscreenCanvas as any).convertToBlob({
106 | type: imageFile.file.type || "image/png",
107 | quality: 1,
108 | });
109 | const file = new File([blob], imageFile.file.name ?? "image.png", {
110 | type: blob.type,
111 | });
112 | onChange?.(file, imageFile.md5);
113 | onClose?.();
114 | };
115 |
116 | //取消裁剪
117 | const handleCancel = () => {
118 | onClose?.();
119 | };
120 |
121 | //切换裁剪比例锁定
122 | const handleToggleAspectLock = () => {
123 | if (!completedCrop) {
124 | console.log("无绘图区域");
125 | return;
126 | }
127 | if (aspect) {
128 | setAspect(undefined);
129 | } else if (imageRef.current && crop) {
130 | const { width, height } = imageRef.current;
131 | const imageAspect = width / height;
132 | setAspect(imageAspect);
133 | setCrop(
134 | centerCrop(
135 | makeAspectCrop(
136 | {
137 | unit: "px",
138 | height: completedCrop.height,
139 | },
140 | imageAspect,
141 | width,
142 | height
143 | ),
144 | width,
145 | height
146 | )
147 | );
148 | }
149 | };
150 | /**
151 | * 获取居中Crop
152 | */
153 | const centerAspectCrop = (
154 | mediaWidth: number,
155 | mediaHeight: number,
156 | aspect: number
157 | ) => {
158 | return centerCrop(
159 | makeAspectCrop(
160 | {
161 | unit: "%",
162 | width: 90,
163 | },
164 | aspect,
165 | mediaWidth,
166 | mediaHeight
167 | ),
168 | mediaWidth,
169 | mediaHeight
170 | );
171 | };
172 |
173 | /**
174 | * 图片加载
175 | */
176 | const handleImageLoad = (e: React.SyntheticEvent) => {
177 | const { width, height } = e.currentTarget;
178 | //图片纵横比
179 | const imageAspect = width / height;
180 | //设置裁剪区域
181 | setCrop(centerAspectCrop(width, height, imageAspect));
182 | //设置保持纵横比
183 | setAspect(imageAspect);
184 | };
185 |
186 | /**
187 | * 铺满裁剪区域
188 | */
189 | const handleCropAreaFull = () => {
190 | if (!imageRef.current) {
191 | return;
192 | }
193 | const { width, height } = imageRef.current;
194 | //创建PixelCrop
195 | const pixelCrop: PixelCrop = {
196 | unit: "px",
197 | x: 0,
198 | y: 0,
199 | width,
200 | height,
201 | };
202 | setCrop(pixelCrop);
203 | setCompletedCrop(pixelCrop);
204 | };
205 | /**
206 | * 重置
207 | */
208 | const handleCropAreaReset = () => {
209 | //重置缩放
210 | setFocusValue({
211 | scale: 0,
212 | rotate: 0,
213 | });
214 | //重置裁剪区域
215 | if (crop && imageRef.current) {
216 | const { width, height } = imageRef.current;
217 | const imageAspect = width / height;
218 | setCrop(centerAspectCrop(width, height, imageAspect));
219 | }
220 | };
221 | return (
222 |
223 |
224 |
225 |
裁剪图片
226 |
227 | {aspect ? (
228 |
229 | ) : (
230 |
231 | )}
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
247 |
253 |
254 |
255 |
256 |
257 |
258 |
268 |
281 |
282 |
283 |
284 |
285 |
286 |
292 |
293 |
294 | );
295 | };
296 |
297 | const ImageCropModal: React.FC = ({
298 | imageFile,
299 | isOpen,
300 | onClose,
301 | onChange,
302 | }) => {
303 | return (
304 |
308 |
313 |
314 | );
315 | };
316 |
317 | export default ImageCropModal;
318 |
--------------------------------------------------------------------------------
/src/components/List/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef, useEffect } from "react";
2 | interface SelectProps {
3 | options: any[];
4 | checkedIndex: number;
5 | onChange?: (value: any) => void;
6 | className?: string;
7 | renderSelected?: (option?: any) => JSX.Element | string;
8 | renderList?: (option?: any) => JSX.Element;
9 | renderFooter?: (option?: any) => JSX.Element;
10 | disabled?: boolean;
11 | listNumber?: number;
12 | }
13 |
14 | const Select: React.FC = ({
15 | options,
16 | checkedIndex,
17 | className,
18 | onChange,
19 | renderSelected,
20 | renderList,
21 | renderFooter,
22 | disabled,
23 | listNumber = 2,
24 | }) => {
25 | const [isOpen, setIsOpen] = useState(false);
26 | //最大列表高度
27 | const [maxListHeight, setMaxListHeight] = useState(0);
28 | const ulRef = useRef(null);
29 | const rootRef = useRef(null);
30 | /**
31 | * 选项点击事件
32 | * @param index 选中的option的index
33 | */
34 | const handleOptionClick = (index: number) => {
35 | onChange?.(index);
36 | setIsOpen(false);
37 | };
38 | //渲染选中的内容
39 | const SelectedRender = (props: { option: any }) => {
40 | if (props.option === undefined) {
41 | return null;
42 | }
43 | if (renderSelected) {
44 | return <>{renderSelected(props.option)}>;
45 | }
46 | return <>{props.option?.toString()}>;
47 | };
48 | //渲染list内容
49 | const ListRender = (props: { option: any }) => {
50 | if (renderList) {
51 | return renderList(props.option);
52 | }
53 | return {props.option.toString()};
54 | };
55 |
56 | //渲染list底部内容
57 | const FooterRender = () => {
58 | return renderFooter ? renderFooter() : null;
59 | };
60 | //设置最大高度
61 | useEffect(() => {
62 | if (isOpen && ulRef.current) {
63 | const liDom = ulRef.current.childNodes[0] as HTMLElement;
64 | if (!liDom) {
65 | setMaxListHeight(80);
66 | }
67 | //计算最大高度
68 | const maxDivHeight = liDom.offsetHeight * listNumber + 10;
69 | //设置最大高度
70 | setMaxListHeight(maxDivHeight);
71 | }
72 | }, [options, isOpen, listNumber]);
73 | //点击空白处关闭
74 | useEffect(() => {
75 | const handleClick = (e: MouseEvent) => {
76 | if (isOpen) {
77 | const target = e.target as HTMLElement;
78 | if (target.closest(".list-content") === rootRef.current) {
79 | return;
80 | }
81 | setIsOpen(false);
82 | }
83 | };
84 | if (isOpen) {
85 | document.addEventListener("click", handleClick);
86 | } else {
87 | document.removeEventListener("click", handleClick);
88 | }
89 | return () => {
90 | document.removeEventListener("click", handleClick);
91 | };
92 | }, [isOpen]);
93 | return (
94 |
95 |
105 | {isOpen && (
106 |
107 |
112 | {options?.map((option, index) => (
113 | - handleOptionClick(index)}
119 | >
120 |
121 |
122 | ))}
123 |
124 |
125 |
126 |
127 |
128 | )}
129 |
130 | );
131 | };
132 |
133 | export default Select;
134 |
--------------------------------------------------------------------------------
/src/components/ProgressBar/constant.ts:
--------------------------------------------------------------------------------
1 | export const COLOR_CLASS_MAP = {
2 | blue: "from-blue-400 to-blue-500",
3 | red: "from-red-400 to-red-500",
4 | };
5 |
--------------------------------------------------------------------------------
/src/components/ProgressBar/index.tsx:
--------------------------------------------------------------------------------
1 | import { decimalToPercentage } from "@/utils/number";
2 | import React, { useMemo } from "react";
3 | import { COLOR_CLASS_MAP } from "./constant";
4 |
5 | type Props = {
6 | current: number;
7 | total: number;
8 | type: "fraction" | "percentage";
9 | className?: string;
10 | color?: "blue" | "red";
11 | };
12 |
13 | const ProgressBar: React.FC = ({
14 | current,
15 | total,
16 | type = "percentage",
17 | color = "blue",
18 | className,
19 | }) => {
20 | //计算百分比
21 | const percentage = useMemo(
22 | () => decimalToPercentage(current / total),
23 | [current, total]
24 | );
25 | //计算内容
26 | const content = useMemo(() => {
27 | if (type === "fraction") {
28 | return `${current}/${total}`;
29 | }
30 | return `${percentage}%`;
31 | }, [current, percentage, total, type]);
32 |
33 | return (
34 |
37 |
41 |
42 | {content}
43 |
44 |
45 | );
46 | };
47 |
48 | export default ProgressBar;
49 |
--------------------------------------------------------------------------------
/src/components/Ruler/index.tsx:
--------------------------------------------------------------------------------
1 | import { shallowEqual } from "@/utils/object";
2 | import produce from "immer";
3 | import React, {
4 | useCallback,
5 | useEffect,
6 | useMemo,
7 | useRef,
8 | useState,
9 | memo,
10 | } from "react";
11 | import { ReactComponent as SVG_ruler } from "@/assets/svg/ruler.svg";
12 | interface typeProps {
13 | min: number;
14 | max: number;
15 | defaultValue: number;
16 | suffix: string;
17 | }
18 | //不同类型刻度尺的刻度值
19 | interface Props {
20 | defaultScale: typeProps;
21 | defaultRotate: typeProps;
22 | onChange?: (values: { scale: number; rotate: number }) => void;
23 | forceValue?: {
24 | scale?: number;
25 | rotate?: number;
26 | };
27 | }
28 |
29 | const Ruler = ({
30 | defaultScale,
31 | defaultRotate,
32 | onChange,
33 | forceValue,
34 | }: Props) => {
35 | /**
36 | * 根据刻度计算位置
37 | */
38 | const getPosition = (type: typeProps) => {
39 | return (type.defaultValue - type.min) / (type.max - type.min);
40 | };
41 | //鼠标按下位置
42 | const mouseDownPosition = useRef<{
43 | xPos: number;
44 | yPos: number;
45 | }>({
46 | xPos: 0,
47 | yPos: 0,
48 | });
49 | //刻度尺的ref
50 | const rulerRef = useRef(null);
51 | //记录当前值来对values做diff
52 | const valuesRef = useRef<{
53 | scale: number;
54 | rotate: number;
55 | } | null>(null);
56 | //正在拖动
57 | const [dragging, setDragging] = useState(false);
58 | //正在操作的对象
59 | const [activeType, setActiveType] = useState<"scale" | "rotate">("scale");
60 | //当前位置
61 | const [position, setPosition] = useState({
62 | scale: getPosition(defaultScale),
63 | rotate: getPosition(defaultRotate),
64 | });
65 | //刻度尺类型
66 | const typeObj: { key: "scale" | "rotate"; name: string }[] = [
67 | { key: "scale", name: "缩放" },
68 | { key: "rotate", name: "旋转" },
69 | ];
70 | /**
71 | * 鼠标按下,开始拖动
72 | * @param event 事件对象
73 | */
74 | const handleMouseDown = (event: React.MouseEvent) => {
75 | event.preventDefault();
76 | mouseDownPosition.current = {
77 | xPos: event.clientX,
78 | yPos: event.clientY,
79 | };
80 | setDragging(true);
81 | };
82 | /**
83 | * 鼠标松开,停止拖动
84 | */
85 | const handleMouseUp = useCallback(() => {
86 | setDragging(false);
87 | }, []);
88 | /**
89 | * 鼠标移动
90 | * @param event 事件对象
91 | */
92 | const handleMouseMove = useCallback(
93 | (event: MouseEvent) => {
94 | if (dragging && rulerRef.current && mouseDownPosition.current) {
95 | //刻度尺位置
96 | const { width: nodeWidth } = rulerRef.current.getBoundingClientRect();
97 | //鼠标位置
98 | const { xPos } = mouseDownPosition.current;
99 | const { clientX, clientY } = event;
100 | //鼠标移动距离
101 | const offsetX = xPos - clientX;
102 | //当前类型对应的位置
103 | const posi = position[activeType];
104 | //鼠标移动距离占刻度尺宽度的百分比
105 | const percentage = Math.max(0, Math.min(1, posi + offsetX / nodeWidth));
106 | setPosition(
107 | produce((draft) => {
108 | draft[activeType] = percentage;
109 | })
110 | );
111 | //更新当前鼠标点击位置
112 | mouseDownPosition.current = {
113 | xPos: clientX,
114 | yPos: clientY,
115 | };
116 | }
117 | },
118 | [activeType, dragging, position]
119 | );
120 |
121 | useEffect(() => {
122 | if (dragging) {
123 | document.addEventListener("mouseup", handleMouseUp);
124 | document.addEventListener("mousemove", handleMouseMove);
125 | } else {
126 | document.removeEventListener("mouseup", handleMouseUp);
127 | document.removeEventListener("mousemove", handleMouseMove);
128 | }
129 | return () => {
130 | document.removeEventListener("mouseup", handleMouseUp);
131 | document.removeEventListener("mousemove", handleMouseMove);
132 | };
133 | }, [dragging, handleMouseMove, handleMouseUp]);
134 |
135 | /**
136 | * 切换刻度尺类型
137 | */
138 | const handleTypeChange = (option: "scale" | "rotate") => {
139 | setActiveType(option);
140 | };
141 |
142 | /**
143 | * 根据当前位置和类型计算当前数值
144 | */
145 | const values = useMemo(() => {
146 | const scaleValue = Math.round(
147 | position["scale"] * (defaultScale.max - defaultScale.min) +
148 | defaultScale.min
149 | );
150 | const rotateValue = Math.round(
151 | position["rotate"] * (defaultRotate.max - defaultRotate.min) +
152 | defaultRotate.min
153 | );
154 | return {
155 | scale: scaleValue,
156 | rotate: rotateValue,
157 | };
158 | }, [
159 | defaultRotate.max,
160 | defaultRotate.min,
161 | defaultScale.max,
162 | defaultScale.min,
163 | position,
164 | ]);
165 |
166 | /**
167 | * 触发回调函数
168 | */
169 | useEffect(() => {
170 | //如果当前值和上一次值相同,不触发回调
171 | if (
172 | values.rotate === valuesRef.current?.rotate &&
173 | values.scale === valuesRef.current?.scale
174 | ) {
175 | return;
176 | }
177 | //记录当前数值
178 | valuesRef.current = values;
179 | //避免小数点后面出现很多位数的情况
180 | const scale = 1 + values.scale / 100;
181 | onChange?.({
182 | scale: Math.round(scale * 100) / 100,
183 | rotate: values.rotate,
184 | });
185 | // eslint-disable-next-line react-hooks/exhaustive-deps
186 | }, [values]);
187 |
188 | /**
189 | * 外部传入的scale和rotate变更会引起内部scale和rotate变化
190 | * 由于组件已在外部做diff,所以可以直接监听两对象变化
191 | */
192 | useEffect(() => {
193 | setPosition(
194 | produce((draft) => {
195 | if (forceValue?.scale !== undefined) {
196 | draft.scale = getPosition({
197 | ...defaultScale,
198 | defaultValue: forceValue.scale,
199 | });
200 | }
201 | if (forceValue?.rotate !== undefined) {
202 | draft.rotate = getPosition({
203 | ...defaultRotate,
204 | defaultValue: forceValue.rotate,
205 | });
206 | }
207 | })
208 | );
209 | }, [forceValue, defaultScale, defaultRotate]);
210 |
211 | return (
212 | <>
213 |
217 |
218 |
219 |
220 |
221 |
222 | {values[activeType]}
223 | {(activeType === "scale" ? defaultScale : defaultRotate).suffix}
224 |
225 |
233 |
234 |
235 | {typeObj.map((item, index) => (
236 |
247 | ))}
248 |
249 |
250 | >
251 | );
252 | };
253 |
254 | export default memo(Ruler, (prevProps, nextProps) => {
255 | return (
256 | shallowEqual(prevProps.defaultRotate, nextProps.defaultRotate) &&
257 | shallowEqual(prevProps.defaultScale, nextProps.defaultScale) &&
258 | prevProps.onChange === nextProps.onChange &&
259 | prevProps.forceValue === nextProps.forceValue
260 | );
261 | });
262 |
--------------------------------------------------------------------------------
/src/components/Table/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { TableProps } from "./type";
3 |
4 | function Table({ columns, data }: TableProps) {
5 | return (
6 |
7 |
8 |
9 | {columns.map((column) => (
10 |
15 | {column.title}
16 | |
17 | ))}
18 |
19 |
20 |
21 | {data.map((row) => (
22 |
23 | {columns.map((column) => (
24 |
28 | {row[column.key] ?? "-"}
29 | |
30 | ))}
31 |
32 | ))}
33 |
34 |
35 | );
36 | }
37 |
38 | export default Table;
39 |
--------------------------------------------------------------------------------
/src/components/Table/type.ts:
--------------------------------------------------------------------------------
1 | export interface TableColumn {
2 | title: string;
3 | key: string;
4 | }
5 | export interface TableData {
6 | id: string;
7 | [key: string]: any;
8 | }
9 |
10 | export interface TableProps {
11 | columns: TableColumn[];
12 | data: TableData[];
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/Thumbnail/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { FileType } from "../Upload/type";
3 |
4 | export const Thumbnail = ({
5 | file,
6 | onClick,
7 | }: {
8 | file: FileType;
9 | onClick?: () => void;
10 | }) => {
11 | //阻止默认事件
12 | const handlePreventDefault = (event: React.MouseEvent) => {
13 | event.preventDefault();
14 | };
15 | //处理点击事件
16 | const handleClick = (event: React.MouseEvent) => {
17 | handlePreventDefault(event);
18 | onClick?.();
19 | };
20 | return (
21 |
27 |
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/components/Tip/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | export const Tip = ({ initShow }: { initShow: boolean }) => {
4 | //是否显示提示
5 | const [show, setShow] = useState(initShow);
6 | //是否销毁结构
7 | const [destroy, setDestroy] = useState(!initShow);
8 |
9 | /**
10 | * 关闭提示
11 | */
12 | const handleClose = () => {
13 | //先进入隐藏动画, 动画结束再销毁
14 | setShow(false);
15 | setTimeout(() => {
16 | setDestroy(true);
17 | }, 151);
18 | };
19 | return (
20 | <>
21 | {!destroy && (
22 |
27 | 此浏览器可能不是基于Chromium内核, 可能存在功能缺失的情况
28 |
32 | x
33 |
34 |
35 | )}
36 | >
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/src/components/Upload/index.tsx:
--------------------------------------------------------------------------------
1 | import { calculateMD5, file2FileType } from "@/utils/file";
2 | import React, { Fragment, useEffect, useState } from "react";
3 | import Dropzone from "react-dropzone";
4 | import pLimit from "p-limit";
5 | import { produce } from "immer";
6 | import { FileType } from "@/components/Upload/type";
7 | import ImageCrop from "../ImageCrop";
8 | import { ReactComponent as SVG_plus } from "@/assets/svg/plus.svg";
9 | import { ReactComponent as SVG_delete } from "@/assets/svg/delete.svg";
10 | import { Thumbnail } from "../Thumbnail";
11 |
12 | const Upload: React.FC<{
13 | list?: FileType[];
14 | disabled?: boolean;
15 | className?: string;
16 | onAdd?: (files: FileType[], insertIndex: number) => void;
17 | onRemove?: (md5: string) => void;
18 | onUploadStateChange?: (status: boolean) => void;
19 | }> = ({ list, disabled, className, onAdd, onRemove, onUploadStateChange }) => {
20 | const [files, setFiles] = useState(list || []);
21 | //打开图片裁剪模态框
22 | const [isModalOpen, setIsModalOpen] = useState(false);
23 | //要编辑的图片
24 | const [editImageFile, setEditImageFile] = useState(null);
25 | //当外部传入的list时,该组件为受控组件
26 | useEffect(() => {
27 | if (list !== undefined) {
28 | setFiles(list);
29 | }
30 | }, [list]);
31 |
32 | /**
33 | * 过滤重复文件
34 | * @param fileList 文件列表
35 | * @param acceptedMD5s 已经计算的md5列表,用于优化性能
36 | * @returns 不重复的文件列表
37 | */
38 | const filterDuplicateFiles = (
39 | fileList: File[],
40 | acceptedMD5s: string[]
41 | ): Promise[] => {
42 | //限制并发数为3
43 | const limit = pLimit(3);
44 | //记录当前文件的md5
45 | const fileHashSet = new Set(files.map((file) => file.md5));
46 | //过滤重复文件
47 | const noDuplicateList = fileList.map(async (file, index) => {
48 | const newFile = await limit(() =>
49 | file2FileType(file, acceptedMD5s[index])
50 | );
51 | if (!fileHashSet.has(newFile.md5)) {
52 | fileHashSet.add(newFile.md5);
53 | return newFile;
54 | }
55 | return null;
56 | });
57 | return noDuplicateList;
58 | };
59 |
60 | const handleDrop = async (acceptedFiles: File[]) => {
61 | if (disabled) {
62 | return;
63 | }
64 | onUploadStateChange?.(true);
65 | await handleAdd(acceptedFiles);
66 | onUploadStateChange?.(false);
67 | };
68 |
69 | const handleAdd = async (
70 | acceptedFiles: File[],
71 | acceptedMD5s: string[] = [],
72 | insertIndex = files.length
73 | ) => {
74 | if (disabled) {
75 | return;
76 | }
77 | //过滤重复文件
78 | const promiseFiles = filterDuplicateFiles(acceptedFiles, acceptedMD5s);
79 | //等待文件计算MD5完成
80 | for (let i = 0; i < promiseFiles.length; i += 3) {
81 | //每次处理3个
82 | const result = await Promise.all(promiseFiles.slice(i, i + 3));
83 | //过滤掉重复文件
84 | const resultWithoutNull = result.filter(
85 | (item): item is FileType => item !== null
86 | );
87 | if (resultWithoutNull.length > 0) {
88 | //当外部没有传入的list时,该组件为非受控组件,直接更新状态
89 | if (list === undefined) {
90 | console.log("[[非受控组件]]: Add File");
91 | setFiles(
92 | produce((draftState) => {
93 | draftState.splice(insertIndex + i, 0, ...resultWithoutNull);
94 | })
95 | );
96 | } else {
97 | //通知外部变更
98 | onAdd?.(resultWithoutNull, insertIndex + i);
99 | }
100 | }
101 | }
102 | };
103 |
104 | const handleRemove = (md5: string) => {
105 | if (disabled) {
106 | return;
107 | }
108 | //当外部没有传入的list时,该组件为非受控组件,直接更新状态
109 | if (list === undefined) {
110 | const fileIndex = files.findIndex((file) => file.md5 === md5);
111 | if (fileIndex !== -1) {
112 | const file = files[fileIndex];
113 | URL.revokeObjectURL(file.src);
114 | URL.revokeObjectURL(file.thumbnail.src);
115 | console.log("[[非受控组件]]: Remove File");
116 | setFiles(
117 | produce((draft) => {
118 | draft.splice(fileIndex, 1);
119 | })
120 | );
121 | }
122 | }
123 | //通知外部变更
124 | onRemove?.(md5);
125 | };
126 | /**
127 | * 图片裁剪模态框关闭回调
128 | */
129 | const handleImageCropChange = async (cropFile: File, originMD5: string) => {
130 | const md5 = await calculateMD5(cropFile);
131 | if (md5 === originMD5) {
132 | return;
133 | }
134 | const index = files.findIndex((file) => file.md5 === originMD5);
135 | handleRemove(originMD5);
136 | handleAdd([cropFile], [md5], index);
137 | };
138 |
139 | /**
140 | * 打开图片裁剪模态框
141 | */
142 | const handleOpenModal = (image: FileType) => {
143 | if (disabled) {
144 | return;
145 | }
146 | setIsModalOpen(true);
147 | setEditImageFile(image);
148 | };
149 | /**
150 | * 关闭图片裁剪模态框
151 | */
152 | const handleCloseModal = () => {
153 | setIsModalOpen(false);
154 | setEditImageFile(null);
155 | };
156 | return (
157 |
158 |
159 |
160 | {({ getRootProps, getInputProps }) => (
161 |
162 |
166 |
167 |
168 |
169 |
170 | )}
171 |
172 | {files.map((file) => (
173 |
177 | handleOpenModal(file)} />
178 |
179 |
185 |
186 | ))}
187 |
188 |
194 |
195 | );
196 | };
197 |
198 | export default Upload;
199 |
--------------------------------------------------------------------------------
/src/components/Upload/type.ts:
--------------------------------------------------------------------------------
1 | export type FileType = {
2 | file: File;
3 | md5: string;
4 | src: string;
5 | thumbnail: {
6 | file: File;
7 | src: string;
8 | };
9 | };
10 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | * {
6 | margin: 0;
7 | padding: 0;
8 | }
9 |
10 | html,
11 | body,
12 | #root {
13 | width: 100%;
14 | height: 100%;
15 | min-width: 375px;
16 | }
17 | /* 滚动条优化 */
18 | *::-webkit-scrollbar {
19 | width: 16px;
20 | height: 16px;
21 | }
22 | *::-webkit-scrollbar-button,
23 | *::-webkit-scrollbar-corner {
24 | display: none;
25 | }
26 | *::-webkit-scrollbar-thumb {
27 | background-color: #909090;
28 | border-radius: 50px;
29 | background-clip: padding-box;
30 | border: 4px solid transparent;
31 | }
32 | *::-webkit-scrollbar-thumb:hover {
33 | background-color: #606060;
34 | border: 3px solid transparent;
35 | }
36 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from "react";
2 | import ReactDOM from "react-dom/client";
3 | import "./index.css";
4 | import "flowbite";
5 | import { BrowserRouter, useRoutes } from "react-router-dom";
6 | import Layout from "./Layout";
7 | import routes from "./routes";
8 |
9 | function App() {
10 | const element = useRoutes(routes);
11 | return (
12 |
13 | {element}
14 |
15 | );
16 | }
17 |
18 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
19 |
20 |
21 |
22 |
23 |
24 | );
25 |
--------------------------------------------------------------------------------
/src/pages/encryption/ControlPanel/constant.ts:
--------------------------------------------------------------------------------
1 | //可操作选项
2 | export const OPTION_CARDS = [
3 | {
4 | title: "加密图像",
5 | description: "使用本算法加密所有图像",
6 | },
7 | {
8 | title: "解密图像",
9 | description: "使用本算法解密所有图像",
10 | },
11 | ];
12 | //图像格式
13 | export const IMAGE_FORMATS = [
14 | {
15 | label: "源格式",
16 | value: "",
17 | },
18 | {
19 | label: "PNG",
20 | value: "image/png",
21 | },
22 | {
23 | label: "JPEG",
24 | value: "image/jpeg",
25 | },
26 | {
27 | label: "BMP",
28 | value: "image/bmp",
29 | },
30 | {
31 | label: "WEBP",
32 | value: "image/webp",
33 | },
34 | ];
35 |
--------------------------------------------------------------------------------
/src/pages/encryption/ControlPanel/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment, useEffect, useMemo, useState } from "react";
2 | import { Plugin } from "@/service/plugin/type";
3 | import List from "@/components/List";
4 | import Button from "@/components/Button";
5 | import { IMAGE_FORMATS, OPTION_CARDS } from "./constant";
6 | import { capitalizeFirstLetter } from "@/utils/string";
7 | import { ControlOptionType, ImageFormatType } from "./type";
8 | import CardSelect from "@/components/CardSelect";
9 |
10 | const Item = (props: {
11 | label: string;
12 | children?: React.ReactNode;
13 | message?: string;
14 | }) => {
15 | return (
16 |
17 |
21 | {props.label}
22 |
23 | {props.children}
24 | {props.message && (
25 |
26 | {props.message}
27 |
28 | )}
29 |
30 | );
31 | };
32 |
33 | export default function ControlPanel({
34 | onStart, //开始加密
35 | className, //类名
36 | pluginList, //插件列表
37 | disabled, //是否正在加密
38 | onClearUpload, //清除上传文件
39 | onClearOutput, //清除输出文件
40 | }: {
41 | onStart?: (option: ControlOptionType) => void;
42 | className?: string;
43 | pluginList: Plugin[];
44 | disabled?: boolean;
45 | onClearUpload?: () => void;
46 | onClearOutput?: () => void;
47 | }) {
48 | //插件索引
49 | const [pluginIndex, setPluginIndex] = useState(0);
50 | //选项卡名称
51 | const [optionName, setOptionName] = useState<"encrypt" | "decrypt">(
52 | "encrypt"
53 | );
54 | //密钥
55 | const [key, setKey] = useState("");
56 | //密钥错误信息
57 | const [keyErrorMessage, setKeyErrorMessage] = useState("");
58 | //图像格式索引
59 | const [formatIndex, setFormatIndex] = useState(0);
60 | //图片质量
61 | const [quality, setQuality] = useState(100);
62 | //是否禁用图像质量
63 | const [isQualityDisabled, setIsQualityDisabled] = useState(false);
64 | /**
65 | * 校验输入密钥
66 | */
67 | const validateKey = (key: string, keyRule?: Plugin["keyRule"] | null) => {
68 | if (keyRule?.required && key === "") {
69 | setKeyErrorMessage("秘钥不能为空");
70 | return false;
71 | } else if (
72 | keyRule?.required &&
73 | keyRule?.regex &&
74 | !new RegExp(keyRule.regex).test(key)
75 | ) {
76 | setKeyErrorMessage(keyRule.message);
77 | return false;
78 | }
79 | setKeyErrorMessage("");
80 | return true;
81 | };
82 | /**
83 | * 开始加密
84 | */
85 | const handleStart = () => {
86 | //校验密钥
87 | const plugin = pluginList[pluginIndex];
88 | if (!validateKey(key, plugin.keyRule)) {
89 | return;
90 | }
91 | onStart?.({
92 | pluginName: plugin.name,
93 | optionName,
94 | key,
95 | format: IMAGE_FORMATS[formatIndex].value as ImageFormatType,
96 | quality: quality / 100,
97 | });
98 | };
99 |
100 | /**
101 | * 选择算法插件改变
102 | * @param pluginName 插件名称
103 | */
104 | const handlePluginChange = (index: number) => {
105 | setPluginIndex(index);
106 | };
107 |
108 | /**
109 | * 要执行的操作改变
110 | * @param event 事件对象
111 | */
112 | const handleOptionChange = (value: number) => {
113 | const options: ["encrypt", "decrypt"] = ["encrypt", "decrypt"];
114 | setOptionName(options[value]);
115 | };
116 |
117 | /**
118 | * 图像质量改变
119 | * @param event 事件对象
120 | */
121 | const handleQualityChange = (event: React.ChangeEvent) => {
122 | const value = parseInt(event.target.value);
123 | if (!isNaN(value) && value >= 0 && value <= 100) {
124 | setQuality(value);
125 | }
126 | };
127 | /**
128 | * 图像格式改变
129 | */
130 | const handleImageFormatChange = (index: number) => {
131 | setFormatIndex(index);
132 | };
133 |
134 | /**
135 | * 密钥改变
136 | * @param event 事件对象
137 | */
138 | const handleKeyChange = (event: React.ChangeEvent) => {
139 | const value = event.target.value;
140 | setKey(value);
141 | //校验密钥
142 | const plugin = pluginList[pluginIndex];
143 | validateKey(value, plugin?.keyRule);
144 | };
145 | /**
146 | * 清空上传文件
147 | */
148 | const handleClearUpload = () => {
149 | onClearUpload?.();
150 | };
151 | /**
152 | * 清空输出文件
153 | */
154 | const handleClearOutput = () => {
155 | onClearOutput?.();
156 | };
157 |
158 | /**
159 | * 渲染插件已选择的内容
160 | */
161 | const renderPluginSelected = (option: Plugin) => {
162 | return option.name ?? "未载入算法";
163 | };
164 |
165 | /**
166 | * 渲染插件列表
167 | */
168 | const renderPluginList = (option: Plugin) => {
169 | return (
170 |
171 | {option.name}
172 | {option.description}
173 |
174 |
175 | {capitalizeFirstLetter(option.language)}
176 |
177 | {option.version}
178 |
179 |
180 | );
181 | };
182 |
183 | /**
184 | * 渲染列表的底部
185 | */
186 | const renderPluginListFooter = () => {
187 | return (
188 |
194 | );
195 | };
196 | //图像质量输入框是否禁用
197 | useEffect(() => {
198 | const format = IMAGE_FORMATS[formatIndex].value;
199 | const disabledResult =
200 | format !== "image/jpeg" && format !== "image/webp" && format !== "";
201 | if (disabledResult) {
202 | setQuality(100);
203 | }
204 | setIsQualityDisabled(disabledResult);
205 | }, [formatIndex]);
206 | //图像质量描述
207 | const qualityLabel = useMemo(() => {
208 | if (quality === 100) {
209 | return "无损";
210 | } else if (quality === 0) {
211 | return "最低";
212 | } else {
213 | return `${quality}%`;
214 | }
215 | }, [quality]);
216 |
217 | return (
218 |
219 | {/* 算法列表 */}
220 |
229 |
230 | {/* 选择操作 */}
231 |
237 |
238 | {/* 秘钥 */}
239 |
-
240 |
249 |
250 |
251 | {/* 文件格式 */}
252 |
-
253 |
item.label}
259 | renderList={(list) => list.label}
260 | className="flex-1"
261 | listNumber={3}
262 | >
263 |
264 | {/* 图像质量 */}
265 |
-
266 |
267 |
276 |
277 | {qualityLabel}
278 |
279 |
280 |
281 |
282 |
283 |
291 |
299 |
302 |
303 |
304 | );
305 | }
306 |
--------------------------------------------------------------------------------
/src/pages/encryption/ControlPanel/type.ts:
--------------------------------------------------------------------------------
1 | export type ImageFormatType =
2 | | "image/png"
3 | | "image/jpeg"
4 | | "image/bmp"
5 | | "image/webp"
6 | | "";
7 | export interface ControlOptionType {
8 | pluginName: string;
9 | optionName: "encrypt" | "decrypt";
10 | key: string;
11 | format: ImageFormatType;
12 | quality: number;
13 | }
14 |
--------------------------------------------------------------------------------
/src/pages/encryption/Output/constant.ts:
--------------------------------------------------------------------------------
1 | import { TableColumn } from "@/components/Table/type";
2 |
3 | export const columns: TableColumn[] = [
4 | {
5 | title: "原图",
6 | key: "origin",
7 | },
8 | {
9 | title: "生成图",
10 | key: "current",
11 | },
12 | {
13 | title: "原图大小",
14 | key: "originSize",
15 | },
16 | {
17 | title: "生成图大小",
18 | key: "currentSize",
19 | },
20 | {
21 | title: "压缩率",
22 | key: "compressionRatio",
23 | },
24 | {
25 | title: "操作",
26 | key: "operate",
27 | },
28 | ];
29 |
--------------------------------------------------------------------------------
/src/pages/encryption/Output/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment, useState } from "react";
2 | import Table from "@/components/Table";
3 | import { FileType } from "@/components/Upload/type";
4 | import { columns } from "./constant";
5 | import CustomModal from "@/components/CustomModal";
6 | import { getCompressionRate } from "@/utils/file";
7 | import { ReactComponent as SVG_delete } from "@/assets/svg/delete.svg";
8 | import { Thumbnail } from "@/components/Thumbnail";
9 | import saveAs from "file-saver";
10 | import { formatSize } from "@/utils/number";
11 |
12 | export default function Output({
13 | pairList,
14 | className,
15 | disabled,
16 | onRemove,
17 | }: {
18 | pairList: [FileType, FileType][];
19 | className?: string;
20 | disabled?: boolean;
21 | onRemove?: (md5: string) => void;
22 | }) {
23 | const [isModalOpen, setIsModalOpen] = useState(false);
24 | const [editImage, setEditImage] = useState<{
25 | src: string;
26 | name: string;
27 | } | null>(null);
28 | /**
29 | * 打开图片裁剪模态框
30 | */
31 | const handleOpenModal = (src: string, name: string) => {
32 | setIsModalOpen(true);
33 | setEditImage({
34 | src,
35 | name,
36 | });
37 | };
38 | /**
39 | * 关闭图片裁剪模态框
40 | */
41 | const handleCloseModal = () => {
42 | setIsModalOpen(false);
43 | setEditImage(null);
44 | };
45 | /**
46 | * 删除图像
47 | */
48 | const handleRemove = (md5: string) => {
49 | if (disabled) {
50 | return;
51 | }
52 | onRemove?.(md5);
53 | };
54 | /**
55 | * 下载图像
56 | */
57 | const handleDown = ({ file }: FileType) => {
58 | if (disabled) {
59 | return;
60 | }
61 | saveAs(file, file.name);
62 | };
63 |
64 | /**
65 | * 生成表格数据
66 | */
67 | const generateData = () =>
68 | Array.from(pairList, ([originFile, encryptFile]) => {
69 | const id = originFile.md5;
70 | const origin = (
71 | handleOpenModal(originFile.src, originFile.file.name)}
74 | />
75 | );
76 | const current = (
77 |
80 | handleOpenModal(encryptFile.src, encryptFile.file.name)
81 | }
82 | />
83 | );
84 | const originSize = formatSize(originFile.file.size, "MB");
85 | const currentSize = formatSize(encryptFile.file.size, "MB");
86 | const compressionRatio = getCompressionRate(
87 | originFile.file,
88 | encryptFile.file
89 | );
90 | const operate = (
91 |
92 | handleDown(encryptFile)}
94 | className="cursor-pointer select-none text-blue-500 font-semibold text-sm underline underline-offset-2 mx-2"
95 | >
96 | 下载
97 |
98 | handleRemove(originFile.md5)}
100 | className="cursor-pointer select-none text-red-500 font-semibold text-sm underline underline-offset-2 mx-2"
101 | >
102 | 删除
103 |
104 |
105 | );
106 | return {
107 | id,
108 | origin,
109 | current,
110 | originSize,
111 | currentSize,
112 | compressionRatio: (
113 | 100 ? "text-red-500" : "text-green-500"
116 | }`}
117 | >
118 | {compressionRatio}%
119 |
120 | ),
121 | operate,
122 | };
123 | });
124 |
125 | return (
126 |
127 |
132 |
136 |
156 |
157 |
163 |
164 |
165 | );
166 | }
167 |
--------------------------------------------------------------------------------
/src/pages/encryption/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useMemo, useRef, useState } from "react";
2 | import { produce } from "immer";
3 | import ControlPanel from "./ControlPanel";
4 | import Upload from "@/components/Upload";
5 | import { FileType } from "@/components/Upload/type";
6 | import ProgressBar from "@/components/ProgressBar";
7 | import OutPut from "./Output";
8 | import { ControlOptionType } from "./ControlPanel/type";
9 | import ImageService from "@/service/image";
10 | import { Plugin } from "@/service/plugin/type";
11 | import { ReactComponent as SVG_download } from "@/assets/svg/download.svg";
12 | import { progressStatus } from "@/service/image/type";
13 | import { multipleFileDownload } from "@/utils/zip";
14 |
15 | export default function Encryption() {
16 | //文件列表
17 | const [fileList, setFileList] = useState([]);
18 | //生成列表(两个顺序一致)
19 | const [filePair, setFilePair] = useState<[FileType, FileType][]>([]);
20 | //插件列表
21 | const [pluginList, setPluginList] = useState([]);
22 | //图片服务
23 | const imageService = useRef(null);
24 | //是否正在加密
25 | const [isEncrypting, setIsEncrypting] = useState(false);
26 | //是否正在导出
27 | const [isExporting, setIsExporting] = useState(false);
28 | //是否正在上传
29 | const [isUploading, setIsUploading] = useState(false);
30 | //描述过程信息
31 | const [processMessage, setProcessMessage] = useState("");
32 | //进度条颜色
33 | const [processColor, setProcessColor] = useState<"blue" | "red">("blue");
34 |
35 | /**
36 | * 新增上传文件
37 | */
38 | const handleFileListAdd = (files: FileType[], insertIndex: number) => {
39 | setFileList(
40 | produce((draftState) => {
41 | draftState.splice(insertIndex, 0, ...files);
42 | })
43 | );
44 | };
45 | /**
46 | * 删除生成文件
47 | */
48 | const handleFilePairRemove = (md5: string) => {
49 | const pairIndex = filePair.findIndex((pair) => pair[0].md5 === md5);
50 | if (pairIndex == -1) {
51 | return;
52 | }
53 | setProcessMessage("");
54 | setProcessColor("blue");
55 | //删除生成列表中的文件
56 | const pair = filePair[pairIndex];
57 | if (pair) {
58 | URL.revokeObjectURL(pair[1].src);
59 | URL.revokeObjectURL(pair[1].thumbnail.src);
60 | setFilePair(
61 | produce((draft) => {
62 | draft.splice(pairIndex, 1);
63 | })
64 | );
65 | }
66 | };
67 | /**
68 | * 删除上传文件
69 | */
70 | const handleFileListRemove = (md5: string) => {
71 | const fileIndex = fileList.findIndex((file) => file.md5 === md5);
72 | if (fileIndex == -1) {
73 | return;
74 | }
75 | //删除上传列表中的文件
76 | const file = fileList[fileIndex];
77 | URL.revokeObjectURL(file.src);
78 | URL.revokeObjectURL(file.thumbnail.src);
79 | setFileList(
80 | produce((draft) => {
81 | draft.splice(fileIndex, 1);
82 | })
83 | );
84 | handleFilePairRemove(md5);
85 | };
86 | /**
87 | * 清空上传文件
88 | */
89 | const handleClearUpload = () => {
90 | for (const file of fileList) {
91 | URL.revokeObjectURL(file.src);
92 | URL.revokeObjectURL(file.thumbnail.src);
93 | }
94 | setFileList([]);
95 | //同时清空输出文件
96 | handleClearOutput();
97 | };
98 | /**
99 | * 清空生成文件
100 | */
101 | const handleClearOutput = () => {
102 | for (const pair of filePair) {
103 | URL.revokeObjectURL(pair[1].src);
104 | URL.revokeObjectURL(pair[1].thumbnail.src);
105 | }
106 | setFilePair([]);
107 | setProcessMessage("");
108 | setProcessColor("blue");
109 | };
110 | /**
111 | * 上传状态改变
112 | */
113 | const handleUploadStateChange = (status: boolean) => {
114 | setIsUploading(status);
115 | };
116 | /**
117 | * 初始化图片服务
118 | */
119 | const initImageService = async () => {
120 | try {
121 | imageService.current = new ImageService();
122 | await imageService.current.initService("encryption");
123 | const plugins = imageService.current.getPlugins();
124 | console.log("plugins", plugins);
125 | if (plugins.length) {
126 | setPluginList(plugins);
127 | }
128 | } catch (error) {
129 | console.error(error);
130 | }
131 | };
132 | /**
133 | * 载入图片业务
134 | */
135 | useEffect(() => {
136 | initImageService();
137 | return () => {
138 | imageService.current = null;
139 | };
140 | }, []);
141 |
142 | /**
143 | * 开始加密
144 | */
145 | const handleStart = async (options: ControlOptionType) => {
146 | if (!fileList.length || !imageService.current) {
147 | return;
148 | }
149 | //开始加密
150 | setIsEncrypting(true);
151 | setFilePair([]);
152 | setProcessColor("blue");
153 | try {
154 | //获取结果
155 | console.log("options", options);
156 | //处理过程信息
157 | const progress = (status: progressStatus) => {
158 | setProcessMessage(status.message);
159 | if (status.done && status.error) {
160 | setProcessColor("red");
161 | }
162 | };
163 | const resList = imageService.current.processing(
164 | fileList,
165 | options,
166 | "encryption",
167 | progress
168 | );
169 | //处理加密结果
170 | for await (const item of resList) {
171 | if (!item) {
172 | continue;
173 | }
174 | setFilePair(
175 | produce((draft) => {
176 | draft.push([item[0], item[1]]);
177 | })
178 | );
179 | }
180 | } catch (error) {
181 | console.error(error);
182 | } finally {
183 | setIsEncrypting(false);
184 | }
185 | };
186 | /**
187 | * 下载结果
188 | */
189 | const handleDownload = async () => {
190 | // 如果正在进行加密或者导出则不允许下载
191 | if (isEncrypting || isExporting || !filePair.length) {
192 | return;
193 | }
194 | setIsExporting(true);
195 |
196 | try {
197 | const files = filePair.map(([, { file }]) => file);
198 | await multipleFileDownload(files, "encrypted-images");
199 | } catch (error) {
200 | console.error(error);
201 | } finally {
202 | setIsExporting(false);
203 | }
204 | };
205 |
206 | //是否正在执行某项操作
207 | const isOperating = useMemo(
208 | () => isEncrypting || isExporting || isUploading,
209 | [isEncrypting, isExporting, isUploading]
210 | );
211 |
212 | // 进度条是否显示
213 | const isProgressShow = useMemo(
214 | () => isEncrypting || filePair.length / fileList.length > 0,
215 | [fileList.length, filePair.length, isEncrypting]
216 | );
217 | return (
218 |
219 |
220 |
232 |
244 |
245 |
246 |
251 |
252 |
257 |
258 |
259 | {processMessage}
260 |
261 |
267 |
268 |
274 |
278 |
279 |
280 |
281 |
282 |
283 | );
284 | }
285 |
--------------------------------------------------------------------------------
/src/pages/steganography/ControlPanel/constant.ts:
--------------------------------------------------------------------------------
1 | //可操作选项
2 | export const OPTION_CARDS = [
3 | {
4 | title: "写入信息",
5 | description: "使用当前算法往图像中写入信息",
6 | },
7 | {
8 | title: "提取信息",
9 | description: "使用当前算法从图像中提取信息",
10 | },
11 | ];
12 |
--------------------------------------------------------------------------------
/src/pages/steganography/ControlPanel/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment, useMemo, useState } from "react";
2 | import { Plugin } from "@/service/plugin/type";
3 | import List from "@/components/List";
4 | import Button from "@/components/Button";
5 | import { OPTION_CARDS } from "./constant";
6 | import { capitalizeFirstLetter } from "@/utils/string";
7 | import { ControlOptionType } from "./type";
8 | import CardSelect from "@/components/CardSelect";
9 |
10 | const Item = (props: {
11 | label: string;
12 | children?: React.ReactNode;
13 | message?: string;
14 | }) => {
15 | return (
16 |
17 |
21 | {props.label}
22 |
23 | {props.children}
24 | {props.message && (
25 |
26 | {props.message}
27 |
28 | )}
29 |
30 | );
31 | };
32 |
33 | export default function ControlPanel({
34 | onStart, //开始加密
35 | className, //类名
36 | pluginList, //插件列表
37 | disabled, //是否正在加密
38 | onClearUpload, //清除上传文件
39 | onClearOutput, //清除输出文件
40 | }: {
41 | onStart?: (option: ControlOptionType) => void;
42 | className?: string;
43 | pluginList: Plugin[];
44 | disabled?: boolean;
45 | onClearUpload?: () => void;
46 | onClearOutput?: () => void;
47 | }) {
48 | //插件索引
49 | const [pluginIndex, setPluginIndex] = useState(0);
50 | //选项卡名称
51 | const [optionIndex, setOptionIndex] = useState(0);
52 | //密钥
53 | const [key, setKey] = useState("");
54 | //写入的信息
55 | const [message, setMessage] = useState("");
56 | //信息重复次数
57 | const [repeatCount, setRepeatCount] = useState("1");
58 | //密钥错误信息
59 | const [keyErrorMessage, setKeyErrorMessage] = useState("");
60 | /**
61 | * 校验输入密钥
62 | */
63 | const validateKey = (key: string, keyRule?: Plugin["keyRule"] | null) => {
64 | if (keyRule?.required && key === "") {
65 | setKeyErrorMessage("秘钥不能为空");
66 | return false;
67 | } else if (
68 | keyRule?.required &&
69 | keyRule?.regex &&
70 | !new RegExp(keyRule.regex).test(key)
71 | ) {
72 | setKeyErrorMessage(keyRule.message);
73 | return false;
74 | }
75 | setKeyErrorMessage("");
76 | return true;
77 | };
78 | /**
79 | * 开始加密
80 | */
81 | const handleStart = () => {
82 | //校验密钥
83 | const plugin = pluginList[pluginIndex];
84 | const options: ["encrypt", "decrypt"] = ["encrypt", "decrypt"];
85 | if (!validateKey(key, plugin.keyRule)) {
86 | return;
87 | }
88 | const repeat = Number(repeatCount);
89 | //如果重复次数不是数字或者小于1,显示设置为1
90 | if (!repeat) {
91 | setRepeatCount("1");
92 | }
93 | onStart?.({
94 | pluginName: plugin.name,
95 | optionName: options[optionIndex],
96 | key,
97 | message: optionIndex ? "" : message,
98 | repeat: repeat || 1,
99 | });
100 | };
101 |
102 | /**
103 | * 选择算法插件改变
104 | * @param pluginName 插件名称
105 | */
106 | const handlePluginChange = (index: number) => {
107 | setPluginIndex(index);
108 | };
109 |
110 | /**
111 | * 要执行的操作改变
112 | * @param event 事件对象
113 | */
114 | const handleOptionChange = (value: number) => {
115 | setOptionIndex(value);
116 | };
117 |
118 | /**
119 | * 密钥改变
120 | * @param event 事件对象
121 | */
122 | const handleKeyChange = (event: React.ChangeEvent) => {
123 | const value = event.target.value;
124 | setKey(value);
125 | //校验密钥
126 | const plugin = pluginList[pluginIndex];
127 | validateKey(value, plugin?.keyRule);
128 | };
129 | const handleMessageChange = (event: React.ChangeEvent) => {
130 | setMessage(event.target.value);
131 | };
132 | const handleRepeatCountChange = (
133 | event: React.ChangeEvent
134 | ) => {
135 | setRepeatCount(event.target.value.trim());
136 | };
137 | /**
138 | * 清空上传文件
139 | */
140 | const handleClearUpload = () => {
141 | onClearUpload?.();
142 | };
143 | /**
144 | * 清空输出文件
145 | */
146 | const handleClearOutput = () => {
147 | onClearOutput?.();
148 | };
149 |
150 | /**
151 | * 渲染插件已选择的内容
152 | */
153 | const renderPluginSelected = (option: Plugin) => {
154 | return option.name ?? "未载入算法";
155 | };
156 |
157 | /**
158 | * 渲染插件列表
159 | */
160 | const renderPluginList = (option: Plugin) => {
161 | return (
162 |
163 | {option.name}
164 | {option.description}
165 |
166 |
167 | {capitalizeFirstLetter(option.language)}
168 |
169 | {option.version}
170 |
171 |
172 | );
173 | };
174 | /**
175 | * 计算当前插件的组件规则
176 | */
177 | const pluginComponentRule = useMemo(() => {
178 | return pluginList[pluginIndex]?.componentRule ?? {};
179 | }, [pluginIndex, pluginList]);
180 |
181 | return (
182 |
265 | );
266 | }
267 |
--------------------------------------------------------------------------------
/src/pages/steganography/ControlPanel/type.ts:
--------------------------------------------------------------------------------
1 | export interface ControlOptionType {
2 | pluginName: string;
3 | optionName: "encrypt" | "decrypt";
4 | key: string;
5 | message: string | null;
6 | repeat: number;
7 | }
8 |
--------------------------------------------------------------------------------
/src/pages/steganography/Output/constant.ts:
--------------------------------------------------------------------------------
1 | import { TableColumn } from "@/components/Table/type";
2 |
3 | export const columns: TableColumn[] = [
4 | {
5 | title: "隐写图",
6 | key: "image",
7 | },
8 | {
9 | title: "原图大小",
10 | key: "originSize",
11 | },
12 | {
13 | title: "隐写图大小",
14 | key: "currentSize",
15 | },
16 | {
17 | title: "压缩率",
18 | key: "compressionRatio",
19 | },
20 | {
21 | title: "承载信息",
22 | key: "message",
23 | },
24 | {
25 | title: "操作",
26 | key: "operate",
27 | },
28 | ];
29 |
--------------------------------------------------------------------------------
/src/pages/steganography/Output/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment, useState } from "react";
2 | import Table from "@/components/Table";
3 | import { FileType } from "@/components/Upload/type";
4 | import { columns } from "./constant";
5 | import CustomModal from "@/components/CustomModal";
6 | import { getCompressionRate } from "@/utils/file";
7 | import { ReactComponent as SVG_delete } from "@/assets/svg/delete.svg";
8 | import { Thumbnail } from "@/components/Thumbnail";
9 | import saveAs from "file-saver";
10 | import { formatSize } from "@/utils/number";
11 |
12 | export default function Output({
13 | pairList,
14 | className,
15 | disabled,
16 | onRemove,
17 | }: {
18 | pairList: [FileType, FileType, string][];
19 | className?: string;
20 | disabled?: boolean;
21 | onRemove?: (md5: string) => void;
22 | }) {
23 | const [isModalOpen, setIsModalOpen] = useState(false);
24 | const [editImage, setEditImage] = useState<{
25 | src: string;
26 | name: string;
27 | } | null>(null);
28 | /**
29 | * 打开图片模态框
30 | */
31 | const handleOpenModal = (src: string, name: string) => {
32 | setIsModalOpen(true);
33 | setEditImage({
34 | src,
35 | name,
36 | });
37 | };
38 | /**
39 | * 关闭图片模态框
40 | */
41 | const handleCloseModal = () => {
42 | setIsModalOpen(false);
43 | setEditImage(null);
44 | };
45 | /**
46 | * 删除图像
47 | */
48 | const handleRemove = (md5: string) => {
49 | if (disabled) {
50 | return;
51 | }
52 | onRemove?.(md5);
53 | };
54 | /**
55 | * 下载图像
56 | */
57 | const handleDown = ({ file }: FileType) => {
58 | if (disabled) {
59 | return;
60 | }
61 | saveAs(file, file.name);
62 | };
63 |
64 | /**
65 | * 生成表格数据
66 | */
67 | const generateData = () =>
68 | Array.from(pairList, ([originFile, encryptFile, payload]) => {
69 | const id = originFile.md5;
70 | const image = (
71 |
74 | handleOpenModal(encryptFile.src, encryptFile.file.name)
75 | }
76 | />
77 | );
78 | const originSize = formatSize(originFile.file.size, "MB");
79 | const currentSize = formatSize(encryptFile.file.size, "MB");
80 | const compressionRatio = getCompressionRate(
81 | originFile.file,
82 | encryptFile.file
83 | );
84 | const operate = (
85 |
86 | handleDown(encryptFile)}
88 | className="cursor-pointer select-none text-blue-500 font-semibold text-sm underline underline-offset-2 mx-2"
89 | >
90 | 下载
91 |
92 | handleRemove(originFile.md5)}
94 | className="cursor-pointer select-none text-red-500 font-semibold text-sm underline underline-offset-2 mx-2"
95 | >
96 | 删除
97 |
98 |
99 | );
100 | return {
101 | id,
102 | image,
103 | originSize,
104 | currentSize,
105 | compressionRatio: (
106 | 100 ? "text-red-500" : "text-green-500"
109 | }`}
110 | >
111 | {compressionRatio}%
112 |
113 | ),
114 | message: (
115 |
116 | {payload}
117 |
118 | ),
119 | operate,
120 | };
121 | });
122 |
123 | return (
124 |
125 |
130 |
134 |
154 |
155 |
161 |
162 |
163 | );
164 | }
165 |
--------------------------------------------------------------------------------
/src/pages/steganography/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useMemo, useRef, useState } from "react";
2 | import { produce } from "immer";
3 | import ControlPanel from "./ControlPanel";
4 | import Upload from "@/components/Upload";
5 | import { FileType } from "@/components/Upload/type";
6 | import ProgressBar from "@/components/ProgressBar";
7 | import OutPut from "./Output";
8 | import { ControlOptionType } from "./ControlPanel/type";
9 | import ImageService from "@/service/image";
10 | import { Plugin } from "@/service/plugin/type";
11 | import { ReactComponent as SVG_download } from "@/assets/svg/download.svg";
12 | import { progressStatus } from "@/service/image/type";
13 | import { multipleFileDownload } from "@/utils/zip";
14 |
15 | export default function Encryption() {
16 | //文件列表
17 | const [fileList, setFileList] = useState([]);
18 | //生成列表(两个顺序一致)
19 | const [filePair, setFilePair] = useState<[FileType, FileType, string][]>([]);
20 | //插件列表
21 | const [pluginList, setPluginList] = useState([]);
22 | //图片服务
23 | const imageService = useRef(null);
24 | //是否正在加密
25 | const [isEncrypting, setIsEncrypting] = useState(false);
26 | //是否正在导出
27 | const [isExporting, setIsExporting] = useState(false);
28 | //是否正在上传
29 | const [isUploading, setIsUploading] = useState(false);
30 | //描述过程信息
31 | const [processMessage, setProcessMessage] = useState("");
32 | //进度条颜色
33 | const [processColor, setProcessColor] = useState<"blue" | "red">("blue");
34 |
35 | /**
36 | * 新增上传文件
37 | */
38 | const handleFileListAdd = (files: FileType[], insertIndex: number) => {
39 | setFileList(
40 | produce((draftState) => {
41 | draftState.splice(insertIndex, 0, ...files);
42 | })
43 | );
44 | };
45 | /**
46 | * 删除生成文件
47 | */
48 | const handleFilePairRemove = (md5: string) => {
49 | const pairIndex = filePair.findIndex((pair) => pair[0].md5 === md5);
50 | if (pairIndex == -1) {
51 | return;
52 | }
53 | setProcessMessage("");
54 | setProcessColor("blue");
55 | //删除生成列表中的文件
56 | const pair = filePair[pairIndex];
57 | if (pair) {
58 | URL.revokeObjectURL(pair[1].src);
59 | URL.revokeObjectURL(pair[1].thumbnail.src);
60 | setFilePair(
61 | produce((draft) => {
62 | draft.splice(pairIndex, 1);
63 | })
64 | );
65 | }
66 | };
67 | /**
68 | * 删除上传文件
69 | */
70 | const handleFileListRemove = (md5: string) => {
71 | const fileIndex = fileList.findIndex((file) => file.md5 === md5);
72 | if (fileIndex == -1) {
73 | return;
74 | }
75 | //删除上传列表中的文件
76 | const file = fileList[fileIndex];
77 | URL.revokeObjectURL(file.src);
78 | URL.revokeObjectURL(file.thumbnail.src);
79 | setFileList(
80 | produce((draft) => {
81 | draft.splice(fileIndex, 1);
82 | })
83 | );
84 | handleFilePairRemove(md5);
85 | };
86 | /**
87 | * 清空上传文件
88 | */
89 | const handleClearUpload = () => {
90 | for (const file of fileList) {
91 | URL.revokeObjectURL(file.src);
92 | URL.revokeObjectURL(file.thumbnail.src);
93 | }
94 | setFileList([]);
95 | //同时清空输出文件
96 | handleClearOutput();
97 | };
98 | /**
99 | * 清空生成文件
100 | */
101 | const handleClearOutput = () => {
102 | for (const pair of filePair) {
103 | URL.revokeObjectURL(pair[1].src);
104 | URL.revokeObjectURL(pair[1].thumbnail.src);
105 | }
106 | setFilePair([]);
107 | setProcessMessage("");
108 | setProcessColor("blue");
109 | };
110 | /**
111 | * 上传状态改变
112 | */
113 | const handleUploadStateChange = (status: boolean) => {
114 | setIsUploading(status);
115 | };
116 | /**
117 | * 初始化图片服务
118 | */
119 | const initImageService = async () => {
120 | try {
121 | imageService.current = new ImageService();
122 | await imageService.current.initService("steganography");
123 | const plugins = imageService.current.getPlugins();
124 | console.log("plugins", plugins);
125 | if (plugins.length) {
126 | setPluginList(plugins);
127 | }
128 | } catch (error) {
129 | console.error(error);
130 | }
131 | };
132 | /**
133 | * 载入图片业务
134 | */
135 | useEffect(() => {
136 | initImageService();
137 | return () => {
138 | imageService.current = null;
139 | };
140 | }, []);
141 |
142 | /**
143 | * 开始加密
144 | */
145 | const handleStart = async (options: ControlOptionType) => {
146 | if (!fileList.length || !imageService.current) {
147 | return;
148 | }
149 | //开始加密
150 | setIsEncrypting(true);
151 | setFilePair([]);
152 | setProcessColor("blue");
153 | try {
154 | //获取结果
155 | console.log("options", options);
156 | //处理过程信息
157 | const progress = (status: progressStatus) => {
158 | setProcessMessage(status.message);
159 | if (status.done && status.error) {
160 | setProcessColor("red");
161 | }
162 | };
163 | const resList = imageService.current.processing(
164 | fileList,
165 | options,
166 | "steganography",
167 | progress
168 | );
169 | //处理加密结果
170 | for await (const item of resList) {
171 | if (!item) {
172 | continue;
173 | }
174 | setFilePair(
175 | produce((draft) => {
176 | draft.push(item);
177 | })
178 | );
179 | }
180 | } catch (error) {
181 | console.error(error);
182 | } finally {
183 | setIsEncrypting(false);
184 | }
185 | };
186 | /**
187 | * 下载结果
188 | */
189 | const handleDownload = async () => {
190 | // 如果正在进行加密或者导出则不允许下载
191 | if (isEncrypting || isExporting || !filePair.length) {
192 | return;
193 | }
194 | setIsExporting(true);
195 |
196 | try {
197 | const files = filePair.map(([, { file }]) => file);
198 | await multipleFileDownload(files, "encrypted-images");
199 | } catch (error) {
200 | console.error(error);
201 | } finally {
202 | setIsExporting(false);
203 | }
204 | };
205 |
206 | //是否正在执行某项操作
207 | const isOperating = useMemo(
208 | () => isEncrypting || isExporting || isUploading,
209 | [isEncrypting, isExporting, isUploading]
210 | );
211 |
212 | // 进度条是否显示
213 | const isProgressShow = useMemo(
214 | () => isEncrypting || filePair.length / fileList.length > 0,
215 | [fileList.length, filePair.length, isEncrypting]
216 | );
217 | return (
218 |
219 |
220 |
232 |
244 |
245 |
246 |
251 |
252 |
257 |
258 |
259 | {processMessage}
260 |
261 |
267 |
268 |
274 |
278 |
279 |
280 |
281 |
282 |
283 | );
284 | }
285 |
--------------------------------------------------------------------------------
/src/plugins/encryption/Arnold/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Arnold变换",
3 | "key": "encry_arnold",
4 | "version": "1.0.0",
5 | "description": "基于Arnold变换, 对图像像素位置置乱, 加密结果是正方形",
6 | "language": "javascript",
7 | "keyRule": {
8 | "regex": "^0*(100|[1-9][0-9]?)$",
9 | "required": true,
10 | "message": "请输入数字范围为1-100"
11 | }
12 | }
--------------------------------------------------------------------------------
/src/plugins/encryption/Arnold/index.ts:
--------------------------------------------------------------------------------
1 | import { PixelBuffer } from "@/service/image/type";
2 | import { processImageByWebGL2 } from "@/utils/webgl";
3 | import { padImageToSquare, restoreImageFromSquare } from "@/utils/file";
4 |
5 | type encryptFuncType = (
6 | data: PixelBuffer,
7 | key: string
8 | ) => {
9 | data: PixelBuffer;
10 | };
11 | type decryptFuncType = encryptFuncType;
12 |
13 | const encrypt: encryptFuncType = (data, key) => {
14 | // 将图像填充为正方形
15 | const paddedData = padImageToSquare(data);
16 | // 片段着色器
17 | const fragmentShader = `#version 300 es
18 | precision mediump float;
19 | uniform int u_iteration;
20 | uniform float u_size;
21 | uniform sampler2D u_texture;
22 | in vec2 v_texcoord;
23 | out vec4 outColor;
24 | void main() {
25 | // 将纹理坐标转换为像素坐标
26 | ivec2 pixelCoordinate = ivec2(v_texcoord * u_size);
27 | // 定义变换矩阵
28 | const mat2 transformMatrix = mat2(1.0, 1.0, 1.0, 2.0);
29 | //迭代变换
30 | for (int i = 0; i < u_iteration; i++) {
31 | // 将 pixelCoordinate 转换为 vec2 类型以进行矩阵乘法
32 | vec2 tempPixelCoordinate = vec2(pixelCoordinate);
33 | // 计算矩阵乘积
34 | tempPixelCoordinate = transformMatrix * tempPixelCoordinate;
35 | // 将结果转换回 ivec2 类型
36 | pixelCoordinate = ivec2(tempPixelCoordinate);
37 | // 为结果加上 int(u_size)
38 | pixelCoordinate = pixelCoordinate + int(u_size);
39 | // 对结果取模 u_size
40 | pixelCoordinate = pixelCoordinate % int(u_size);
41 | }
42 |
43 | // 将获取到的颜色值设置为输出颜色
44 | outColor = texelFetch(u_texture, pixelCoordinate, 0);
45 | }
46 | `;
47 |
48 | // 处理函数
49 | const process = (gl: WebGL2RenderingContext, program: WebGLProgram) => {
50 | // 获取 u_iteration 变量位置
51 | const iterationsLocation = gl.getUniformLocation(program, "u_iteration");
52 | gl.uniform1i(iterationsLocation, Number(key));
53 | // 获取 u_size 变量位置
54 | const sizeLocation = gl.getUniformLocation(program, "u_size");
55 | gl.uniform1f(sizeLocation, paddedData.width);
56 | };
57 |
58 | // 返回输出数据
59 | return {
60 | data: processImageByWebGL2(paddedData, fragmentShader, process),
61 | };
62 | };
63 | const decrypt: decryptFuncType = (data, key) => {
64 | // 片段着色器
65 | const fragmentShader = `#version 300 es
66 | precision highp float;
67 | uniform int u_iterations;
68 | uniform float u_size;
69 | uniform sampler2D u_texture;
70 | in vec2 v_texcoord;
71 | out vec4 outColor;
72 | void main() {
73 | // 将纹理坐标转换为像素坐标
74 | ivec2 pixelCoordinate = ivec2(v_texcoord * u_size);
75 | // 定义变换矩阵
76 | const mat2 transformMatrix = mat2(2.0, -1.0, -1.0, 1.0);
77 | for (int i = 0; i < u_iterations; i++) {
78 | // 将 pixelCoordinate 转换为 vec2 类型以进行矩阵乘法
79 | vec2 tempPixelCoordinate = vec2(pixelCoordinate);
80 | // 计算矩阵乘积
81 | tempPixelCoordinate = transformMatrix * tempPixelCoordinate;
82 | // 将结果转换回 ivec2 类型
83 | pixelCoordinate = ivec2(tempPixelCoordinate);
84 | // 为结果加上 int(u_size)
85 | pixelCoordinate = pixelCoordinate + int(u_size);
86 | // 对结果取模 u_size
87 | pixelCoordinate = pixelCoordinate % int(u_size);
88 | }
89 | outColor = texelFetch(u_texture, pixelCoordinate, 0);
90 | }
91 | `;
92 |
93 | // 处理函数
94 | const process = (gl: WebGL2RenderingContext, program: WebGLProgram) => {
95 | // 获取 u_iterations 变量位置
96 | const iterationsLocation = gl.getUniformLocation(program, "u_iterations");
97 | gl.uniform1i(iterationsLocation, Number(key));
98 | // 获取 u_size 变量位置
99 | const sizeLocation = gl.getUniformLocation(program, "u_size");
100 | gl.uniform1f(sizeLocation, data.width);
101 | };
102 |
103 | // 进行Arnold变换
104 | const transformData = processImageByWebGL2(data, fragmentShader, process);
105 | //裁剪图像
106 | return {
107 | data: restoreImageFromSquare(transformData),
108 | };
109 | };
110 |
111 | export { encrypt, decrypt };
112 |
--------------------------------------------------------------------------------
/src/plugins/encryption/DNA/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "基于DNA编码的混沌加密算法",
3 | "key": "encry_dna",
4 | "version": "1.0.0",
5 | "description": "基于DNA动态编码及混沌Logistic映射的加密算法",
6 | "language": "javascript",
7 | "keyRule": {
8 | "regex": "",
9 | "required": true,
10 | "message": ""
11 | }
12 | }
--------------------------------------------------------------------------------
/src/plugins/encryption/DNA/index.ts:
--------------------------------------------------------------------------------
1 | import { PixelBuffer } from "@/service/image/type";
2 | import { DNAByte } from "@/utils/dna";
3 | import { PBKDF2 } from "crypto-js";
4 | import { str2Num } from "@/utils/string";
5 | import {
6 | dnaByte2Byte,
7 | byte2DNAByte,
8 | add4Rule1,
9 | sub4Rule1,
10 | xor4Rule1,
11 | } from "@/utils/dna";
12 |
13 | type encryptFuncType = (
14 | data: PixelBuffer,
15 | key: string
16 | ) => {
17 | data: PixelBuffer;
18 | };
19 | type decryptFuncType = encryptFuncType;
20 | type DNAByte3Channel = [DNAByte, DNAByte, DNAByte];
21 | /**
22 | * 默认规则编码->在默认规则编码的前提下用某种方式和前一个像素计算->动态规则解码
23 | * @param data 图像数据
24 | * @param key 密钥
25 | * @returns 加密后的图像数据
26 | */
27 | const encrypt: encryptFuncType = ({ buffer, width, height, name }, key) => {
28 | // 初始化
29 | const pixels = new Uint8ClampedArray(buffer);
30 | const midPixels = new Uint8ClampedArray(pixels.length);
31 |
32 | //规则对应操作
33 | const funcMap = {
34 | 0: add4Rule1,
35 | 1: xor4Rule1,
36 | 2: sub4Rule1,
37 | };
38 | // logistic映射函数
39 | const logistic = (x: number) => {
40 | const mu = 3.999999999999999;
41 | return mu * x * (1 - x);
42 | };
43 |
44 | //派生密钥
45 | //keys[0-2]用于确定不同通道动态规则
46 | //keys[3]用于确定运算规则
47 | const keys = Array.from({ length: 4 }, (_, i) =>
48 | PBKDF2(key, `key${i}`).toString()
49 | );
50 | //额外的密钥用于确定初始状态
51 | //用于确定默认规则和扩散初始值
52 | const extraKeys = Array.from({ length: 3 }, (_, i) =>
53 | PBKDF2(key, `extraKey${i}`).toString()
54 | );
55 | //将密钥转换为初始状态
56 | let states = keys.map((key) => str2Num(key, true));
57 | //将额外密钥转换为初始状态
58 | let extraStates = extraKeys.map((key) => str2Num(key, true));
59 | //初始迭代1000次
60 | for (let i = 0; i < 1000; i++) {
61 | states = states.map(logistic);
62 | extraStates = extraStates.map(logistic);
63 | }
64 | //确定默认规则
65 | const defaultRule = Math.round(extraStates[0] * 7);
66 | //确定扩散初始值
67 | const prevDNAByte3Channel: DNAByte3Channel = [
68 | byte2DNAByte(
69 | Math.round(extraStates[2] * 255),
70 | Math.round(extraStates[0] * 7)
71 | ),
72 | byte2DNAByte(
73 | Math.round(extraStates[1] * 255),
74 | Math.round(extraStates[1] * 7)
75 | ),
76 | byte2DNAByte(
77 | Math.round(extraStates[0] * 255),
78 | Math.round(extraStates[2] * 7)
79 | ),
80 | ];
81 | //使用混沌序列对图像进行加密
82 | for (let h = 0; h < height; h++) {
83 | for (let w = 0; w < width; w++) {
84 | const index = (h * width + w) * 4;
85 | //迭代混沌序列
86 | states = states.map(logistic);
87 |
88 | // 使用默认编码规则编码三个通道
89 | const currDNAByte3Channel: DNAByte3Channel = [
90 | byte2DNAByte(pixels[index], defaultRule),
91 | byte2DNAByte(pixels[index + 1], defaultRule),
92 | byte2DNAByte(pixels[index + 2], defaultRule),
93 | ];
94 |
95 | //根据key[1]确定运算规则, 逐通道运算
96 | const ruleFunc = funcMap[Math.round(states[3] * 2) as 0 | 1 | 2];
97 | for (let i = 0; i < prevDNAByte3Channel.length; i++) {
98 | const prevDNAByte = prevDNAByte3Channel[i];
99 | const currDNAByte = currDNAByte3Channel[i];
100 | // 每个像素包含4个编码
101 | currDNAByte3Channel[i] = prevDNAByte.map((prevDNACode, codeIndex) =>
102 | ruleFunc(currDNAByte[codeIndex], prevDNACode)
103 | ) as DNAByte;
104 |
105 | // 记录当前通道编码
106 | prevDNAByte3Channel[i] = currDNAByte3Channel[i];
107 | }
108 |
109 | // 根据key[0]使用动态解码规则解码三个通道
110 | midPixels[index] = dnaByte2Byte(
111 | currDNAByte3Channel[0],
112 | Math.round(states[0] * 7)
113 | );
114 | midPixels[index + 1] = dnaByte2Byte(
115 | currDNAByte3Channel[1],
116 | Math.round(states[1] * 7)
117 | );
118 | midPixels[index + 2] = dnaByte2Byte(
119 | currDNAByte3Channel[2],
120 | Math.round(states[2] * 7)
121 | );
122 | // 图像A值保持不变
123 | midPixels[index + 3] = pixels[index + 3];
124 | }
125 | }
126 | // 输出
127 | return {
128 | data: {
129 | name,
130 | buffer: midPixels.buffer,
131 | width,
132 | height,
133 | },
134 | };
135 | };
136 | /**
137 | * 动态规则解码->得到计算后的结果, 将结果和前一个的加密结果做反运算->默认规则解码
138 | * @param data 图像数据
139 | * @param key 密钥
140 | * @returns 解密后的图像数据
141 | */
142 |
143 | const decrypt: decryptFuncType = ({ buffer, width, height, name }, key) => {
144 | // 初始化
145 | const pixels = new Uint8ClampedArray(buffer);
146 | const midPixels = new Uint8ClampedArray(pixels.length);
147 | //规则对应操作
148 | const funcMap = {
149 | 0: add4Rule1,
150 | 1: xor4Rule1,
151 | 2: sub4Rule1,
152 | };
153 | // logistic映射函数
154 | const logistic = (x: number) => {
155 | const mu = 3.999999999999999;
156 | return mu * x * (1 - x);
157 | };
158 |
159 | //派生密钥
160 | //keys[0-2]用于确定动态规则
161 | //keys[3]用于确定运算规则
162 | const keys = Array.from({ length: 4 }, (_, i) =>
163 | PBKDF2(key, `key${i}`).toString()
164 | );
165 | //额外的密钥用于确定初始状态
166 | //用于确定默认规则和扩散初始值
167 | const extraKeys = Array.from({ length: 3 }, (_, i) =>
168 | PBKDF2(key, `extraKey${i}`).toString()
169 | );
170 | //将密钥转换为初始状态
171 | let states = keys.map((key) => str2Num(key, true));
172 | //将额外密钥转换为初始状态
173 | let extraStates = extraKeys.map((key) => str2Num(key, true));
174 | //初始迭代1000次
175 | for (let i = 0; i < 1000; i++) {
176 | states = states.map(logistic);
177 | extraStates = extraStates.map(logistic);
178 | }
179 | //确定默认规则
180 | const defaultRule = Math.round(extraStates[0] * 7);
181 | //确定扩散初始值
182 | const prevDNAByte3Channel: DNAByte3Channel = [
183 | byte2DNAByte(
184 | Math.round(extraStates[2] * 255),
185 | Math.round(extraStates[0] * 7)
186 | ),
187 | byte2DNAByte(
188 | Math.round(extraStates[1] * 255),
189 | Math.round(extraStates[1] * 7)
190 | ),
191 | byte2DNAByte(
192 | Math.round(extraStates[0] * 255),
193 | Math.round(extraStates[2] * 7)
194 | ),
195 | ];
196 | //使用混沌序列对图像进行解密
197 | for (let h = 0; h < height; h++) {
198 | for (let w = 0; w < width; w++) {
199 | const index = (h * width + w) * 4;
200 | //迭代混沌序列
201 | states = states.map(logistic);
202 |
203 | // 根据key[0]使用动态编码规则编码三个通道,还原到计算后的状态
204 | const currDNAByte3Channel: DNAByte3Channel = [
205 | byte2DNAByte(pixels[index], Math.round(states[0] * 7)),
206 | byte2DNAByte(pixels[index + 1], Math.round(states[1] * 7)),
207 | byte2DNAByte(pixels[index + 2], Math.round(states[2] * 7)),
208 | ];
209 |
210 | //根据key[1]确定运算规则, 逐通道逆运算
211 | const ruleFunc = funcMap[(2 - Math.round(states[3] * 2)) as 0 | 1 | 2];
212 | for (let i = 0; i < prevDNAByte3Channel.length; i++) {
213 | const prevDNAByte = prevDNAByte3Channel[i];
214 | const currDNAByte = currDNAByte3Channel[i];
215 | // 每个像素包含4个编码
216 | // 因为当前像素加密时使用的值是前一个像素加密后的值,所以为了后面的像素解密, 这里也应该是当前像素的加密值
217 | prevDNAByte3Channel[i] = currDNAByte3Channel[i];
218 | // 运算解密像素
219 | currDNAByte3Channel[i] = prevDNAByte.map((prevDNACode, codeIndex) =>
220 | ruleFunc(currDNAByte[codeIndex], prevDNACode)
221 | ) as DNAByte;
222 | }
223 | // 使用默认解码规则解码三个通道
224 | midPixels[index] = dnaByte2Byte(currDNAByte3Channel[0], defaultRule);
225 | midPixels[index + 1] = dnaByte2Byte(currDNAByte3Channel[1], defaultRule);
226 | midPixels[index + 2] = dnaByte2Byte(currDNAByte3Channel[2], defaultRule);
227 | // 图像A值保持不变
228 | midPixels[index + 3] = pixels[index + 3];
229 | }
230 | }
231 | // 输出
232 | return {
233 | data: {
234 | name,
235 | buffer: midPixels.buffer,
236 | width,
237 | height,
238 | },
239 | };
240 | };
241 |
242 | export { encrypt, decrypt };
243 |
--------------------------------------------------------------------------------
/src/plugins/encryption/Logistic/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "混沌Logistic加密算法",
3 | "key": "encry_logistic",
4 | "version": "1.0.0",
5 | "description": "基于混沌Logistic映射对图像进行加密",
6 | "language": "javascript",
7 | "keyRule": {
8 | "regex": "",
9 | "required": true,
10 | "message": ""
11 | }
12 | }
--------------------------------------------------------------------------------
/src/plugins/encryption/Logistic/index.ts:
--------------------------------------------------------------------------------
1 | import { PixelBuffer } from "@/service/image/type";
2 | import { PBKDF2 } from "crypto-js";
3 | import { str2Num } from "@/utils/string";
4 | type encryptFuncType = (
5 | data: PixelBuffer,
6 | key: string
7 | ) => {
8 | data: PixelBuffer;
9 | };
10 | type decryptFuncType = encryptFuncType;
11 |
12 | const encrypt: encryptFuncType = ({ buffer, width, height, name }, key) => {
13 | // 初始化
14 | const pixels = new Uint8ClampedArray(buffer);
15 | const midPixels = new Uint8ClampedArray(pixels.length);
16 | // logistic映射函数
17 | const logistic = (x: number) => {
18 | const mu = 3.999999999999999;
19 | return mu * x * (1 - x);
20 | };
21 |
22 | //派生密钥
23 | const keys = Array.from({ length: 4 }, (_, i) =>
24 | PBKDF2(key, `key${i}`).toString()
25 | );
26 |
27 | //将密钥转换为初始状态
28 | let states = keys.map((key) => str2Num(key, true));
29 | //初始迭代1000次
30 | for (let i = 0; i < 1000; i++) {
31 | states = states.map(logistic);
32 | }
33 |
34 | //使用混沌序列对图像进行加密
35 | for (let i = 0; i < height; i++) {
36 | for (let j = 0; j < width; j++) {
37 | const index = (i * width + j) * 4;
38 | // 对像素进行异或, 不同的state迭代次数和不同通道的状态可以极大程度的去除图像三个通道的相关性
39 | states = states.map(logistic);
40 | // 图像R值
41 | states[0] = logistic(states[0]);
42 | midPixels[index] =
43 | pixels[index] ^
44 | Math.round(states[0] * 255) ^
45 | Math.round(states[1] * 255);
46 |
47 | // 图像G值
48 | states[0] = logistic(states[0]);
49 | midPixels[index + 1] =
50 | pixels[index + 1] ^
51 | Math.round(states[0] * 255) ^
52 | Math.round(states[2] * 255);
53 |
54 | // 图像B值
55 | states[0] = logistic(states[0]);
56 | midPixels[index + 2] =
57 | pixels[index + 2] ^
58 | Math.round(states[0] * 255) ^
59 | Math.round(states[3] * 255);
60 |
61 | // 图像A值保持不变
62 | midPixels[index + 3] = pixels[index + 3];
63 | }
64 | }
65 | // 输出
66 | return {
67 | data: {
68 | name,
69 | buffer: midPixels.buffer,
70 | width,
71 | height,
72 | },
73 | };
74 | };
75 |
76 | const decrypt: decryptFuncType = (data, key) => {
77 | // 解密与加密过程相同
78 | return encrypt(data, key);
79 | };
80 |
81 | export { encrypt, decrypt };
82 |
--------------------------------------------------------------------------------
/src/plugins/encryption/Tent/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "混沌Tent加密算法",
3 | "key": "encry_tent",
4 | "version": "1.0.0",
5 | "description": "基于Tent映射对图像进行加密",
6 | "language": "javascript",
7 | "keyRule": {
8 | "regex": "",
9 | "required": true,
10 | "message": ""
11 | }
12 | }
--------------------------------------------------------------------------------
/src/plugins/encryption/Tent/index.ts:
--------------------------------------------------------------------------------
1 | import { PixelBuffer } from "@/service/image/type";
2 | import { PBKDF2 } from "crypto-js";
3 | import { str2Num } from "@/utils/string";
4 | type encryptFuncType = (
5 | data: PixelBuffer,
6 | key: string
7 | ) => {
8 | data: PixelBuffer;
9 | };
10 | type decryptFuncType = encryptFuncType;
11 |
12 | const encrypt: encryptFuncType = ({ buffer, width, height, name }, key) => {
13 | // 初始化
14 | const pixels = new Uint8ClampedArray(buffer);
15 | const midPixels = new Uint8ClampedArray(pixels.length);
16 | // Tent映射函数
17 | const tentMap = (x: number) => {
18 | const r = 1.999999999999999;
19 | if (x < 0.5) {
20 | x = r * x;
21 | } else {
22 | x = r * (1 - x);
23 | }
24 | return x;
25 | };
26 |
27 | //派生密钥
28 | const keys = Array.from({ length: 4 }, (_, i) =>
29 | PBKDF2(key, `key${i}`).toString()
30 | );
31 |
32 | //将密钥转换为初始状态
33 | let states = keys.map((key) => str2Num(key, true));
34 | //初始迭代1000次
35 | for (let i = 0; i < 1000; i++) {
36 | states = states.map(tentMap);
37 | }
38 |
39 | //使用混沌序列对图像进行加密
40 | for (let i = 0; i < height; i++) {
41 | for (let j = 0; j < width; j++) {
42 | const index = (i * width + j) * 4;
43 | // 对像素进行异或, 不同的state迭代次数和不同通道的状态可以极大程度的去除图像三个通道的相关性
44 | states = states.map(tentMap);
45 | // 图像R值
46 | states[0] = tentMap(states[0]);
47 | midPixels[index] =
48 | pixels[index] ^
49 | Math.round(states[0] * 255) ^
50 | Math.round(states[1] * 255);
51 |
52 | // 图像G值
53 | states[0] = tentMap(states[0]);
54 | midPixels[index + 1] =
55 | pixels[index + 1] ^
56 | Math.round(states[0] * 255) ^
57 | Math.round(states[2] * 255);
58 |
59 | // 图像B值
60 | states[0] = tentMap(states[0]);
61 | midPixels[index + 2] =
62 | pixels[index + 2] ^
63 | Math.round(states[0] * 255) ^
64 | Math.round(states[3] * 255);
65 |
66 | // 图像A值保持不变
67 | midPixels[index + 3] = pixels[index + 3];
68 | }
69 | }
70 | // 输出
71 | return {
72 | data: {
73 | name,
74 | buffer: midPixels.buffer,
75 | width,
76 | height,
77 | },
78 | };
79 | };
80 |
81 | const decrypt: decryptFuncType = (data, key) => {
82 | // 解密与加密过程相同
83 | return encrypt(data, key);
84 | };
85 |
86 | export { encrypt, decrypt };
87 |
--------------------------------------------------------------------------------
/src/plugins/steganography/DCT/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "基于DCT变换的隐写术",
3 | "key": "stega_dct",
4 | "version": "1.0.0",
5 | "description": "基于DCT变换的图像隐写方法",
6 | "language": "javascript",
7 | "keyRule": {
8 | "regex": "",
9 | "required": true,
10 | "message": ""
11 | },
12 | "componentRule": {
13 | "repeat": true
14 | }
15 | }
--------------------------------------------------------------------------------
/src/plugins/steganography/DCT/index.ts:
--------------------------------------------------------------------------------
1 | import { PixelBuffer } from "@/service/image/type";
2 | import { writeMsgToImage, readMsgFromImage } from "./dct";
3 | type encryptFuncType = (
4 | data: PixelBuffer,
5 | key: string,
6 | options: { message: string; repeat: number }
7 | ) => {
8 | data: PixelBuffer;
9 | payload?: string;
10 | };
11 |
12 | type decryptFuncType = (
13 | data: PixelBuffer,
14 | key: string,
15 | options: { repeat: number }
16 | ) => {
17 | data: PixelBuffer;
18 | payload: string;
19 | };
20 | //图像隐写
21 | const encrypt: encryptFuncType = (data, key, { message, repeat }) => {
22 | //如果消息为空,则直接返回原图像
23 | if (!message?.trim()) {
24 | return {
25 | data,
26 | };
27 | }
28 | const resData = writeMsgToImage(data, message, key, repeat);
29 | return {
30 | data: resData,
31 | payload: message,
32 | };
33 | };
34 | //图像提取
35 | const decrypt: decryptFuncType = (data, key, { repeat }) => {
36 | const payload = readMsgFromImage(data, key, repeat);
37 | return {
38 | data,
39 | payload,
40 | };
41 | };
42 |
43 | export { encrypt, decrypt };
44 |
--------------------------------------------------------------------------------
/src/plugins/steganography/DCT/mersenne-twister.d.ts:
--------------------------------------------------------------------------------
1 | declare class MersenneTwister {
2 | constructor(seed?: number);
3 | init_genrand(s: number): void;
4 | init_by_array(init_key: number[], key_length: number): void;
5 | genrand_int32(): number;
6 | }
7 | export default MersenneTwister;
8 |
--------------------------------------------------------------------------------
/src/plugins/steganography/DCT/mersenne-twister.js:
--------------------------------------------------------------------------------
1 | // Code from https://gist.github.com/banksean/300494 for seeded rand.
2 |
3 | /*
4 | I've wrapped Makoto Matsumoto and Takuji Nishimura's code in a namespace
5 | so it's better encapsulated. Now you can have multiple random number generators
6 | and they won't stomp all over eachother's state.
7 |
8 | If you want to use this as a substitute for Math.random(), use the random()
9 | method like so:
10 |
11 | var m = new MersenneTwister();
12 | var randomNumber = m.random();
13 |
14 | You can also call the other genrand_{foo}() methods on the instance.
15 | If you want to use a specific seed in order to get a repeatable random
16 | sequence, pass an integer into the constructor:
17 | var m = new MersenneTwister(123);
18 | and that will always produce the same random sequence.
19 | Sean McCullough (banksean@gmail.com)
20 | */
21 |
22 | /*
23 | A C-program for MT19937, with initialization improved 2002/1/26.
24 | Coded by Takuji Nishimura and Makoto Matsumoto.
25 |
26 | Before using, initialize the state by using init_genrand(seed)
27 | or init_by_array(init_key, key_length).
28 |
29 | Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura,
30 | All rights reserved.
31 |
32 | Redistribution and use in source and binary forms, with or without
33 | modification, are permitted provided that the following conditions
34 | are met:
35 |
36 | 1. Redistributions of source code must retain the above copyright
37 | notice, this list of conditions and the following disclaimer.
38 |
39 | 2. Redistributions in binary form must reproduce the above copyright
40 | notice, this list of conditions and the following disclaimer in the
41 | documentation and/or other materials provided with the distribution.
42 |
43 | 3. The names of its contributors may not be used to endorse or promote
44 | products derived from this software without specific prior written
45 | permission.
46 |
47 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
48 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
49 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
50 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
51 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
52 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
53 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
54 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
55 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
56 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
57 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
58 |
59 |
60 | Any feedback is very welcome.
61 | http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/emt.html
62 | email: m-mat @ math.sci.hiroshima-u.ac.jp (remove space)
63 | */
64 |
65 | var MersenneTwister = function (seed) {
66 | if (seed == undefined) {
67 | seed = new Date().getTime();
68 | }
69 | /* Period parameters */
70 | this.N = 624;
71 | this.M = 397;
72 | this.MATRIX_A = 0x9908b0df; /* constant vector a */
73 | this.UPPER_MASK = 0x80000000; /* most significant w-r bits */
74 | this.LOWER_MASK = 0x7fffffff; /* least significant r bits */
75 |
76 | this.mt = new Array(this.N); /* the array for the state vector */
77 | this.mti = this.N + 1; /* mti==N+1 means mt[N] is not initialized */
78 |
79 | this.init_genrand(seed);
80 | };
81 |
82 | /* initializes mt[N] with a seed */
83 | MersenneTwister.prototype.init_genrand = function (s) {
84 | this.mt[0] = s >>> 0;
85 | for (this.mti = 1; this.mti < this.N; this.mti++) {
86 | const s = this.mt[this.mti - 1] ^ (this.mt[this.mti - 1] >>> 30);
87 | this.mt[this.mti] =
88 | ((((s & 0xffff0000) >>> 16) * 1812433253) << 16) +
89 | (s & 0x0000ffff) * 1812433253 +
90 | this.mti;
91 | /* See Knuth TAOCP Vol2. 3rd Ed. P.106 for multiplier. */
92 | /* In the previous versions, MSBs of the seed affect */
93 | /* only MSBs of the array mt[]. */
94 | /* 2002/01/09 modified by Makoto Matsumoto */
95 | this.mt[this.mti] >>>= 0;
96 | /* for >32 bit machines */
97 | }
98 | };
99 |
100 | /* initialize by an array with array-length */
101 | /* init_key is the array for initializing keys */
102 | /* key_length is its length */
103 | /* slight change for C++, 2004/2/26 */
104 | MersenneTwister.prototype.init_by_array = function (init_key, key_length) {
105 | var i, j, k;
106 | this.init_genrand(19650218);
107 | i = 1;
108 | j = 0;
109 | k = this.N > key_length ? this.N : key_length;
110 | for (; k; k--) {
111 | var s = this.mt[i - 1] ^ (this.mt[i - 1] >>> 30);
112 | this.mt[i] =
113 | (this.mt[i] ^
114 | (((((s & 0xffff0000) >>> 16) * 1664525) << 16) +
115 | (s & 0x0000ffff) * 1664525)) +
116 | init_key[j] +
117 | j; /* non linear */
118 | this.mt[i] >>>= 0; /* for WORDSIZE > 32 machines */
119 | i++;
120 | j++;
121 | if (i >= this.N) {
122 | this.mt[0] = this.mt[this.N - 1];
123 | i = 1;
124 | }
125 | if (j >= key_length) j = 0;
126 | }
127 | for (k = this.N - 1; k; k--) {
128 | const s = this.mt[i - 1] ^ (this.mt[i - 1] >>> 30);
129 | this.mt[i] =
130 | (this.mt[i] ^
131 | (((((s & 0xffff0000) >>> 16) * 1566083941) << 16) +
132 | (s & 0x0000ffff) * 1566083941)) -
133 | i; /* non linear */
134 | this.mt[i] >>>= 0; /* for WORDSIZE > 32 machines */
135 | i++;
136 | if (i >= this.N) {
137 | this.mt[0] = this.mt[this.N - 1];
138 | i = 1;
139 | }
140 | }
141 |
142 | this.mt[0] = 0x80000000; /* MSB is 1; assuring non-zero initial array */
143 | };
144 |
145 | /* generates a random number on [0,0xffffffff]-interval */
146 | MersenneTwister.prototype.genrand_int32 = function () {
147 | var y;
148 | var mag01 = [0x0, this.MATRIX_A];
149 | /* mag01[x] = x * MATRIX_A for x=0,1 */
150 |
151 | if (this.mti >= this.N) {
152 | /* generate N words at one time */
153 | var kk;
154 |
155 | if (this.mti == this.N + 1)
156 | /* if init_genrand() has not been called, */
157 | this.init_genrand(5489); /* a default initial seed is used */
158 |
159 | for (kk = 0; kk < this.N - this.M; kk++) {
160 | y = (this.mt[kk] & this.UPPER_MASK) | (this.mt[kk + 1] & this.LOWER_MASK);
161 | this.mt[kk] = this.mt[kk + this.M] ^ (y >>> 1) ^ mag01[y & 0x1];
162 | }
163 | for (; kk < this.N - 1; kk++) {
164 | y = (this.mt[kk] & this.UPPER_MASK) | (this.mt[kk + 1] & this.LOWER_MASK);
165 | this.mt[kk] =
166 | this.mt[kk + (this.M - this.N)] ^ (y >>> 1) ^ mag01[y & 0x1];
167 | }
168 | y =
169 | (this.mt[this.N - 1] & this.UPPER_MASK) | (this.mt[0] & this.LOWER_MASK);
170 | this.mt[this.N - 1] = this.mt[this.M - 1] ^ (y >>> 1) ^ mag01[y & 0x1];
171 |
172 | this.mti = 0;
173 | }
174 |
175 | y = this.mt[this.mti++];
176 |
177 | /* Tempering */
178 | y ^= y >>> 11;
179 | y ^= (y << 7) & 0x9d2c5680;
180 | y ^= (y << 15) & 0xefc60000;
181 | y ^= y >>> 18;
182 |
183 | return y >>> 0;
184 | };
185 | /* These real versions are due to Isaku Wada, 2002/01/09 added */
186 | export default MersenneTwister;
187 |
--------------------------------------------------------------------------------
/src/plugins/steganography/DCT/utf_8.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 用于将UTF-8编码的字节数组解码为字符串
3 | * @param {*} bytes 字节数组
4 | * @returns 解码后的字符串
5 | */
6 | export function utf8Decode(bytes: number[]): string {
7 | const chars = [];
8 | let offset = 0;
9 | const length = bytes.length;
10 | let c;
11 | let c2;
12 | let c3;
13 |
14 | while (offset < length) {
15 | c = bytes[offset];
16 | c2 = bytes[offset + 1];
17 | c3 = bytes[offset + 2];
18 |
19 | if (128 > c) {
20 | chars.push(String.fromCharCode(c));
21 | offset += 1;
22 | } else if (191 < c && c < 224) {
23 | chars.push(String.fromCharCode(((c & 31) << 6) | (c2 & 63)));
24 | offset += 2;
25 | } else {
26 | chars.push(
27 | String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63))
28 | );
29 | offset += 3;
30 | }
31 | }
32 |
33 | return chars.join("");
34 | }
35 | /**
36 | * 用用于将字符串编码为UTF-8字节数组
37 | * @param {*} str 输入字符串
38 | * @returns 编码后的字节数组
39 | */
40 | export function utf8Encode(str: string) {
41 | const bytes = [];
42 | let offset = 0;
43 | let char;
44 |
45 | str = encodeURI(str);
46 | const length = str.length;
47 |
48 | while (offset < length) {
49 | char = str[offset];
50 | offset += 1;
51 |
52 | if ("%" !== char) {
53 | bytes.push(char.charCodeAt(0));
54 | } else {
55 | char = str[offset] + str[offset + 1];
56 | bytes.push(parseInt(char, 16));
57 | offset += 2;
58 | }
59 | }
60 |
61 | return bytes;
62 | }
63 |
--------------------------------------------------------------------------------
/src/plugins/steganography/lsb/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "LSB隐写",
3 | "key": "stega_lsb",
4 | "version": "1.0.0",
5 | "description": "基于最低有效位的图像隐写方法",
6 | "language": "javascript",
7 | "keyRule": {
8 | "regex": "",
9 | "required": true,
10 | "message": ""
11 | },
12 | "componentRule": {
13 | "repeat": false
14 | }
15 | }
--------------------------------------------------------------------------------
/src/plugins/steganography/lsb/index.ts:
--------------------------------------------------------------------------------
1 | import { PixelBuffer } from "@/service/image/type";
2 | import { writeMsgToImage, readMsgFromImage } from "./lsb";
3 | type encryptFuncType = (
4 | data: PixelBuffer,
5 | key: string,
6 | options: { message: string; repeat: number }
7 | ) => {
8 | data: PixelBuffer;
9 | payload?: string;
10 | };
11 |
12 | type decryptFuncType = (
13 | data: PixelBuffer,
14 | key: string,
15 | options: { repeat: number }
16 | ) => {
17 | data: PixelBuffer;
18 | payload: string;
19 | };
20 | //图像隐写
21 | const encrypt: encryptFuncType = (data, key, { message }) => {
22 | //如果消息为空,则直接返回原图像
23 | if (!message?.trim()) {
24 | return {
25 | data,
26 | };
27 | }
28 | const resData = writeMsgToImage(data, message, key);
29 | return {
30 | data: resData,
31 | payload: message,
32 | };
33 | };
34 | //图像提取
35 | const decrypt: decryptFuncType = (data, key) => {
36 | const payload = readMsgFromImage(data, key);
37 | return {
38 | data,
39 | payload,
40 | };
41 | };
42 |
43 | export { encrypt, decrypt };
44 |
--------------------------------------------------------------------------------
/src/plugins/steganography/lsb/lsb.ts:
--------------------------------------------------------------------------------
1 | import { PixelBuffer } from "@/service/image/type";
2 | import { SHA512 } from "crypto-js";
3 | import MersenneTwister from "./mersenne-twister";
4 | import { utf8Encode, utf8Decode } from "./utf_8";
5 |
6 | /**
7 | * 基于密码生成一个哈希顺序数组
8 | * @param password 用于生成哈希顺序的密码
9 | * @param arr_len 生成的顺序数组长度
10 | * @returns 生成的哈希顺序数组
11 | */
12 | function get_hashed_order(password: string, arr_len: number) {
13 | // O(arr_len) algorithm
14 | const orders = Array.from(Array(arr_len).keys());
15 | const result = [];
16 | let loc;
17 | const seed = SHA512(password).words.reduce(function (total, num) {
18 | return total + Math.abs(num);
19 | }, 0);
20 | const rnd = new MersenneTwister(seed);
21 | for (let i = arr_len; i > 0; i--) {
22 | loc = rnd.genrand_int32() % i;
23 | result.push(orders[loc]);
24 | orders[loc] = orders[i - 1];
25 | }
26 | return result;
27 | }
28 |
29 | /**
30 | * 将字符串转换为二进制数组
31 | * @param str 输入字符串
32 | * @param num_copy 每个字符复制的次数
33 | * @returns 二进制数组
34 | */
35 | function str_to_bits(str: string, num_copy: number) {
36 | const utf8array = utf8Encode(str);
37 | const result = [];
38 | const utf8strlen = utf8array.length;
39 | for (let i = 0; i < utf8strlen; i++) {
40 | for (let j = 128; j > 0; j = Math.floor(j / 2)) {
41 | if (Math.floor(utf8array[i] / j)) {
42 | for (let cp = 0; cp < num_copy; cp++) result.push(1);
43 | utf8array[i] -= j;
44 | } else {
45 | for (let cp = 0; cp < num_copy; cp++) result.push(0);
46 | }
47 | }
48 | }
49 | for (let j = 0; j < 24; j++) {
50 | for (let i = 0; i < num_copy; i++) {
51 | result.push(1);
52 | }
53 | }
54 | return result;
55 | }
56 | /**
57 | * 将二进制数组转换为字符串
58 | * @param bitarray 二进制数组
59 | * @param num_copy 每个字符复制的次数
60 | * @returns 字符串
61 | */
62 | function bits_to_str(bitarray: number[], num_copy: number) {
63 | function merge_bits(bits: number[]) {
64 | const bits_len = bits.length;
65 | let bits_sum = 0;
66 | for (let i = 0; i < bits_len; i++) bits_sum += bits[i];
67 | return Math.round(bits_sum / bits_len);
68 | }
69 |
70 | const msg_array = [];
71 | let data, tmp;
72 |
73 | const msg_array_len = Math.floor(Math.floor(bitarray.length / num_copy) / 8);
74 | for (let i = 0; i < msg_array_len; i++) {
75 | data = 0;
76 | tmp = 128;
77 | for (let j = 0; j < 8; j++) {
78 | data +=
79 | merge_bits(
80 | bitarray.slice((i * 8 + j) * num_copy, (i * 8 + j + 1) * num_copy)
81 | ) * tmp;
82 | tmp = Math.floor(tmp / 2);
83 | }
84 | if (data == 255) break; //END NOTATION
85 | msg_array.push(data);
86 | }
87 |
88 | return utf8Decode(msg_array);
89 | }
90 | /**
91 | * 准备要写入的数据,对数据进行混淆,并按照加密密钥生成的哈希顺序将数据位插入到结果数组中
92 | * @param data_bits 要写入的数据位数组
93 | * @param enc_key 加密密钥
94 | * @param encode_len 要编码的长度
95 | * @returns 处理后的数据位数组
96 | */
97 | function prepare_write_data(
98 | data_bits: number[],
99 | enc_key: string,
100 | encode_len: number
101 | ) {
102 | const data_bits_len = data_bits.length;
103 | if (data_bits.length > encode_len) throw "Can not hold this many data!";
104 | const result = Array(encode_len);
105 | for (let i = 0; i < encode_len; i++) {
106 | result[i] = Math.floor(Math.random() * 2); //obfuscation
107 | }
108 |
109 | const order = get_hashed_order(enc_key, encode_len);
110 | for (let i = 0; i < data_bits_len; i++) result[order[i]] = data_bits[i];
111 |
112 | return result;
113 | }
114 | function write_lsb(imageBuffer: Uint8ClampedArray, setdata: number[]) {
115 | function unsetbit(k: number) {
116 | return k % 2 == 1 ? k - 1 : k;
117 | }
118 |
119 | function setbit(k: number) {
120 | return k % 2 == 1 ? k : k + 1;
121 | }
122 | let j = 0;
123 | const newImageBuffer = new Uint8ClampedArray(imageBuffer.length);
124 |
125 | for (let i = 0; i < newImageBuffer.length; i += 4) {
126 | newImageBuffer[i] = setdata[j]
127 | ? setbit(imageBuffer[i])
128 | : unsetbit(imageBuffer[i]);
129 | newImageBuffer[i + 1] = setdata[j + 1]
130 | ? setbit(imageBuffer[i + 1])
131 | : unsetbit(imageBuffer[i + 1]);
132 | newImageBuffer[i + 2] = setdata[j + 2]
133 | ? setbit(imageBuffer[i + 2])
134 | : unsetbit(imageBuffer[i + 2]);
135 | newImageBuffer[i + 3] = imageBuffer[i + 3];
136 | j += 3;
137 | }
138 | return newImageBuffer.buffer;
139 | }
140 |
141 | /**
142 | * 准备要读取的数据,并按照加密密钥生成的哈希顺序读取数据
143 | * @param data_bits 要写入的数据位数组
144 | * @param enc_key 加密密钥
145 | * @returns 处理后的数据位数组
146 | */
147 | function prepare_read_data(data_bits: number[], enc_key: string) {
148 | const data_bits_len = data_bits.length;
149 | const result = Array(data_bits_len);
150 | const order = get_hashed_order(enc_key, data_bits_len);
151 |
152 | for (let i = 0; i < data_bits_len; i++) result[i] = data_bits[order[i]];
153 |
154 | return result;
155 | }
156 | /**
157 | * 从图像数据中读取数据
158 | * @param imageDataArray 图像数据
159 | * @returns 读取到的数据
160 | */
161 | function get_bits_lsb(imageDataArray: Uint8ClampedArray) {
162 | const result = [];
163 | for (let i = 0; i < imageDataArray.length; i += 4) {
164 | result.push(imageDataArray[i] % 2 == 1 ? 1 : 0);
165 | result.push(imageDataArray[i + 1] % 2 == 1 ? 1 : 0);
166 | result.push(imageDataArray[i + 2] % 2 == 1 ? 1 : 0);
167 | }
168 | return result;
169 | }
170 |
171 | /**
172 | * 从图像中提取消息
173 | * @param pixelData 图像数据
174 | * @param enc_key 用于提取消息的密钥
175 | * @param num_copy 每个位要写入图像的副本数。较大的值具有更强的鲁棒性,但容量较小
176 | * @returns 提取的消息
177 | */
178 | export function readMsgFromImage(
179 | pixelData: PixelBuffer,
180 | enc_key: string,
181 | num_copy = 1
182 | ) {
183 | const imageDataArray = new Uint8ClampedArray(pixelData.buffer);
184 | const tempBitsArray = get_bits_lsb(imageDataArray);
185 | const tempArray = prepare_read_data(tempBitsArray, enc_key);
186 | return bits_to_str(tempArray, num_copy);
187 | }
188 | /**
189 | * 将消息写入图像
190 | * @param pixelData 图像数据
191 | * @param msg 要隐藏的消息
192 | * @param enc_key 用于加密消息的密钥
193 | * @param num_copy 每个位要写入图像的副本数。较大的值具有更强的鲁棒性,但容量较小
194 | * @returns 处理后的图像数据
195 | */
196 | export const writeMsgToImage = (
197 | pixelData: PixelBuffer,
198 | msg: string,
199 | enc_key: string,
200 | num_copy = 1
201 | ): PixelBuffer => {
202 | const imageBuffer = new Uint8ClampedArray(pixelData.buffer);
203 | //保存结果
204 | let resultBuffer = pixelData.buffer;
205 | const encode_len = (imageBuffer.length / 4) * 3;
206 | // prepare data
207 | let bit_stream = str_to_bits(msg, num_copy);
208 | bit_stream = prepare_write_data(bit_stream, enc_key, encode_len);
209 | resultBuffer = write_lsb(imageBuffer, bit_stream);
210 | return {
211 | width: pixelData.width,
212 | height: pixelData.height,
213 | name: pixelData.name,
214 | buffer: resultBuffer,
215 | };
216 | };
217 |
--------------------------------------------------------------------------------
/src/plugins/steganography/lsb/mersenne-twister.d.ts:
--------------------------------------------------------------------------------
1 | declare class MersenneTwister {
2 | constructor(seed?: number);
3 | init_genrand(s: number): void;
4 | init_by_array(init_key: number[], key_length: number): void;
5 | genrand_int32(): number;
6 | }
7 | export default MersenneTwister;
8 |
--------------------------------------------------------------------------------
/src/plugins/steganography/lsb/mersenne-twister.js:
--------------------------------------------------------------------------------
1 | // Code from https://gist.github.com/banksean/300494 for seeded rand.
2 |
3 | /*
4 | I've wrapped Makoto Matsumoto and Takuji Nishimura's code in a namespace
5 | so it's better encapsulated. Now you can have multiple random number generators
6 | and they won't stomp all over eachother's state.
7 |
8 | If you want to use this as a substitute for Math.random(), use the random()
9 | method like so:
10 |
11 | var m = new MersenneTwister();
12 | var randomNumber = m.random();
13 |
14 | You can also call the other genrand_{foo}() methods on the instance.
15 | If you want to use a specific seed in order to get a repeatable random
16 | sequence, pass an integer into the constructor:
17 | var m = new MersenneTwister(123);
18 | and that will always produce the same random sequence.
19 | Sean McCullough (banksean@gmail.com)
20 | */
21 |
22 | /*
23 | A C-program for MT19937, with initialization improved 2002/1/26.
24 | Coded by Takuji Nishimura and Makoto Matsumoto.
25 |
26 | Before using, initialize the state by using init_genrand(seed)
27 | or init_by_array(init_key, key_length).
28 |
29 | Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura,
30 | All rights reserved.
31 |
32 | Redistribution and use in source and binary forms, with or without
33 | modification, are permitted provided that the following conditions
34 | are met:
35 |
36 | 1. Redistributions of source code must retain the above copyright
37 | notice, this list of conditions and the following disclaimer.
38 |
39 | 2. Redistributions in binary form must reproduce the above copyright
40 | notice, this list of conditions and the following disclaimer in the
41 | documentation and/or other materials provided with the distribution.
42 |
43 | 3. The names of its contributors may not be used to endorse or promote
44 | products derived from this software without specific prior written
45 | permission.
46 |
47 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
48 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
49 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
50 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
51 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
52 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
53 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
54 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
55 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
56 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
57 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
58 |
59 |
60 | Any feedback is very welcome.
61 | http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/emt.html
62 | email: m-mat @ math.sci.hiroshima-u.ac.jp (remove space)
63 | */
64 |
65 | var MersenneTwister = function (seed) {
66 | if (seed == undefined) {
67 | seed = new Date().getTime();
68 | }
69 | /* Period parameters */
70 | this.N = 624;
71 | this.M = 397;
72 | this.MATRIX_A = 0x9908b0df; /* constant vector a */
73 | this.UPPER_MASK = 0x80000000; /* most significant w-r bits */
74 | this.LOWER_MASK = 0x7fffffff; /* least significant r bits */
75 |
76 | this.mt = new Array(this.N); /* the array for the state vector */
77 | this.mti = this.N + 1; /* mti==N+1 means mt[N] is not initialized */
78 |
79 | this.init_genrand(seed);
80 | };
81 |
82 | /* initializes mt[N] with a seed */
83 | MersenneTwister.prototype.init_genrand = function (s) {
84 | this.mt[0] = s >>> 0;
85 | for (this.mti = 1; this.mti < this.N; this.mti++) {
86 | const s = this.mt[this.mti - 1] ^ (this.mt[this.mti - 1] >>> 30);
87 | this.mt[this.mti] =
88 | ((((s & 0xffff0000) >>> 16) * 1812433253) << 16) +
89 | (s & 0x0000ffff) * 1812433253 +
90 | this.mti;
91 | /* See Knuth TAOCP Vol2. 3rd Ed. P.106 for multiplier. */
92 | /* In the previous versions, MSBs of the seed affect */
93 | /* only MSBs of the array mt[]. */
94 | /* 2002/01/09 modified by Makoto Matsumoto */
95 | this.mt[this.mti] >>>= 0;
96 | /* for >32 bit machines */
97 | }
98 | };
99 |
100 | /* initialize by an array with array-length */
101 | /* init_key is the array for initializing keys */
102 | /* key_length is its length */
103 | /* slight change for C++, 2004/2/26 */
104 | MersenneTwister.prototype.init_by_array = function (init_key, key_length) {
105 | var i, j, k;
106 | this.init_genrand(19650218);
107 | i = 1;
108 | j = 0;
109 | k = this.N > key_length ? this.N : key_length;
110 | for (; k; k--) {
111 | var s = this.mt[i - 1] ^ (this.mt[i - 1] >>> 30);
112 | this.mt[i] =
113 | (this.mt[i] ^
114 | (((((s & 0xffff0000) >>> 16) * 1664525) << 16) +
115 | (s & 0x0000ffff) * 1664525)) +
116 | init_key[j] +
117 | j; /* non linear */
118 | this.mt[i] >>>= 0; /* for WORDSIZE > 32 machines */
119 | i++;
120 | j++;
121 | if (i >= this.N) {
122 | this.mt[0] = this.mt[this.N - 1];
123 | i = 1;
124 | }
125 | if (j >= key_length) j = 0;
126 | }
127 | for (k = this.N - 1; k; k--) {
128 | const s = this.mt[i - 1] ^ (this.mt[i - 1] >>> 30);
129 | this.mt[i] =
130 | (this.mt[i] ^
131 | (((((s & 0xffff0000) >>> 16) * 1566083941) << 16) +
132 | (s & 0x0000ffff) * 1566083941)) -
133 | i; /* non linear */
134 | this.mt[i] >>>= 0; /* for WORDSIZE > 32 machines */
135 | i++;
136 | if (i >= this.N) {
137 | this.mt[0] = this.mt[this.N - 1];
138 | i = 1;
139 | }
140 | }
141 |
142 | this.mt[0] = 0x80000000; /* MSB is 1; assuring non-zero initial array */
143 | };
144 |
145 | /* generates a random number on [0,0xffffffff]-interval */
146 | MersenneTwister.prototype.genrand_int32 = function () {
147 | var y;
148 | var mag01 = [0x0, this.MATRIX_A];
149 | /* mag01[x] = x * MATRIX_A for x=0,1 */
150 |
151 | if (this.mti >= this.N) {
152 | /* generate N words at one time */
153 | var kk;
154 |
155 | if (this.mti == this.N + 1)
156 | /* if init_genrand() has not been called, */
157 | this.init_genrand(5489); /* a default initial seed is used */
158 |
159 | for (kk = 0; kk < this.N - this.M; kk++) {
160 | y = (this.mt[kk] & this.UPPER_MASK) | (this.mt[kk + 1] & this.LOWER_MASK);
161 | this.mt[kk] = this.mt[kk + this.M] ^ (y >>> 1) ^ mag01[y & 0x1];
162 | }
163 | for (; kk < this.N - 1; kk++) {
164 | y = (this.mt[kk] & this.UPPER_MASK) | (this.mt[kk + 1] & this.LOWER_MASK);
165 | this.mt[kk] =
166 | this.mt[kk + (this.M - this.N)] ^ (y >>> 1) ^ mag01[y & 0x1];
167 | }
168 | y =
169 | (this.mt[this.N - 1] & this.UPPER_MASK) | (this.mt[0] & this.LOWER_MASK);
170 | this.mt[this.N - 1] = this.mt[this.M - 1] ^ (y >>> 1) ^ mag01[y & 0x1];
171 |
172 | this.mti = 0;
173 | }
174 |
175 | y = this.mt[this.mti++];
176 |
177 | /* Tempering */
178 | y ^= y >>> 11;
179 | y ^= (y << 7) & 0x9d2c5680;
180 | y ^= (y << 15) & 0xefc60000;
181 | y ^= y >>> 18;
182 |
183 | return y >>> 0;
184 | };
185 | /* These real versions are due to Isaku Wada, 2002/01/09 added */
186 | export default MersenneTwister;
187 |
--------------------------------------------------------------------------------
/src/plugins/steganography/lsb/utf_8.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 用于将UTF-8编码的字节数组解码为字符串
3 | * @param {*} bytes 字节数组
4 | * @returns 解码后的字符串
5 | */
6 | export function utf8Decode(bytes: number[]): string {
7 | const chars = [];
8 | let offset = 0;
9 | const length = bytes.length;
10 | let c;
11 | let c2;
12 | let c3;
13 |
14 | while (offset < length) {
15 | c = bytes[offset];
16 | c2 = bytes[offset + 1];
17 | c3 = bytes[offset + 2];
18 |
19 | if (128 > c) {
20 | chars.push(String.fromCharCode(c));
21 | offset += 1;
22 | } else if (191 < c && c < 224) {
23 | chars.push(String.fromCharCode(((c & 31) << 6) | (c2 & 63)));
24 | offset += 2;
25 | } else {
26 | chars.push(
27 | String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63))
28 | );
29 | offset += 3;
30 | }
31 | }
32 |
33 | return chars.join("");
34 | }
35 | /**
36 | * 用用于将字符串编码为UTF-8字节数组
37 | * @param {*} str 输入字符串
38 | * @returns 编码后的字节数组
39 | */
40 | export function utf8Encode(str: string) {
41 | const bytes = [];
42 | let offset = 0;
43 | let char;
44 |
45 | str = encodeURI(str);
46 | const length = str.length;
47 |
48 | while (offset < length) {
49 | char = str[offset];
50 | offset += 1;
51 |
52 | if ("%" !== char) {
53 | bytes.push(char.charCodeAt(0));
54 | } else {
55 | char = str[offset] + str[offset + 1];
56 | bytes.push(parseInt(char, 16));
57 | offset += 2;
58 | }
59 | }
60 |
61 | return bytes;
62 | }
63 |
--------------------------------------------------------------------------------
/src/routes.tsx:
--------------------------------------------------------------------------------
1 | import React, { Navigate, RouteObject } from "react-router-dom";
2 | import { lazy } from "react";
3 | const Encryption = lazy(() => import("./pages/encryption"));
4 | const Steganography = lazy(() => import("./pages/steganography"));
5 |
6 | export type CustomRouteObject = RouteObject & {
7 | name?: string;
8 | description?: string;
9 | children?: CustomRouteObject[];
10 | };
11 | const routes: CustomRouteObject[] = [
12 | {
13 | name: "图像加密",
14 | path: "/encryption",
15 | description:
16 | "对图像进行批量加密处理, 请选择对应算法、上传图片、输入密钥、点击开始按钮进行操作\r\n请注意已加密图像不抗压缩攻击, 请勿进行有损压缩\r\n图像格式是否可用取决于浏览器支持",
17 | element: ,
18 | },
19 | {
20 | name: "图像隐写",
21 | path: "/steganography",
22 | description:
23 | "对图像进行批量隐写处理, 请选择对应算法、上传图片、输入密钥、点击开始按钮进行隐写\r\n请注意已隐写图像不抗压缩攻击, 请勿进行有损压缩\r\n更多的信息重复次数可以增加鲁棒性",
24 | element: ,
25 | },
26 | {
27 | path: "*",
28 | element: ,
29 | },
30 | ] as CustomRouteObject[];
31 |
32 | export default routes;
33 |
--------------------------------------------------------------------------------
/src/service/cache/index.ts:
--------------------------------------------------------------------------------
1 | import { FileType } from "@/components/Upload/type";
2 | //文件缓存服务
3 | export class FileMD5Cache {
4 | private cache: Set = new Set();
5 |
6 | public add(file: FileType) {
7 | this.cache.add(file.md5);
8 | }
9 |
10 | public has(file: FileType) {
11 | return this.cache.has(file.md5);
12 | }
13 |
14 | public isAllHas(files: FileType[]) {
15 | return files.every(this.has);
16 | }
17 |
18 | public filterNoHas(files: FileType[]) {
19 | return files.filter((file) => !this.has(file));
20 | }
21 |
22 | public filterHas(files: FileType[]) {
23 | return files.filter(this.has);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/service/image/index.ts:
--------------------------------------------------------------------------------
1 | import { FileType } from "@/components/Upload/type";
2 | import { ControlOptionType as OptionEncryType } from "@/pages/encryption/ControlPanel/type";
3 | import { ControlOptionType as OptionStegaType } from "@/pages/steganography/ControlPanel/type";
4 | import PluginService from "@/service/plugin";
5 | import { Plugin } from "@/service/plugin/type";
6 | import { getThreadsNumber } from "@/utils";
7 | import { createURL4FileType } from "@/utils/file";
8 | import WorkService from "../worker";
9 | import { PluginJson, progressStatus } from "./type";
10 | import WorkerThread from "./worker?worker";
11 | /**
12 | * 图像服务
13 | */
14 | class ImageService {
15 | private readonly pluginService: PluginService = new PluginService(); //插件服务
16 | /**
17 | * 初始化,不处理任何错误,直接抛出
18 | */
19 | public async initService(module: "encryption" | "steganography") {
20 | const modules =
21 | module === "encryption"
22 | ? import.meta.glob("@/plugins/encryption/**")
23 | : import.meta.glob("@/plugins/steganography/**");
24 | const modulesKeySet = new Set();
25 | //获取所有插件的配置文件
26 | Object.keys(modules).forEach((key) => {
27 | if (/index.json$/.test(key)) {
28 | modulesKeySet.add(key.replace(/\.json$/, ""));
29 | }
30 | });
31 | //读取配置信息,载入插件
32 | const initResultPromise = Array.from(
33 | modulesKeySet,
34 | (key) =>
35 | new Promise((res, rej) => {
36 | const load = async () => {
37 | const pluginJson = (await modules[`${key}.json`]()) as PluginJson;
38 | return this.loadPlugin(pluginJson.default);
39 | };
40 | load().then(res).catch(rej);
41 | })
42 | );
43 | //插件载入结果
44 | await Promise.all(initResultPromise);
45 | return true;
46 | }
47 | /**
48 | * 加载插件,不处理任何错误,直接抛出
49 | */
50 | public loadPlugin(plugin: Plugin) {
51 | if (!this.pluginService) {
52 | throw new Error("插件服务未初始化");
53 | }
54 | return this.pluginService.loadPlugin(plugin);
55 | }
56 | /**
57 | * 获取插件列表
58 | * @returns Plugin[]
59 | */
60 | public getPlugins() {
61 | if (!this.pluginService) {
62 | console.error("插件服务未初始化");
63 | return [];
64 | }
65 | return this.pluginService.getPlugins();
66 | }
67 | /**
68 | * 获取算法实例,不处理任何错误,直接抛出
69 | * @param pluginName 插件名称
70 | * @returns 插件实例
71 | */
72 | private getPlugin(pluginName: string) {
73 | if (!this.pluginService) {
74 | throw new Error("插件服务未初始化");
75 | }
76 | if (!pluginName) {
77 | throw new Error("未选择插件");
78 | }
79 | return this.pluginService.getPlugin(pluginName);
80 | }
81 |
82 | /**
83 | * 图像处理,不处理错误直接抛出
84 | * @param files 图像列表
85 | * @param options 详细操作
86 | * @param onprogress 进度回调
87 | * @returns
88 | */
89 | public processing(
90 | files: FileType[],
91 | options: OptionEncryType | OptionStegaType,
92 | type: "encryption" | "steganography",
93 | onprogress: (status: progressStatus) => void
94 | ) {
95 | //获取算法实例
96 | onprogress?.({
97 | done: false,
98 | message: "正在获取算法实例...",
99 | error: null,
100 | });
101 | const { pluginName, optionName, key } = options;
102 | const { key: pluginKey } = this.getPlugin(pluginName);
103 | if (files.length === 0) {
104 | return [];
105 | }
106 | //获取较优线程数,并实例化多线程服务
107 | onprogress?.({
108 | done: false,
109 | message: "正在初始化多线程服务...",
110 | error: null,
111 | });
112 | const threadNum = getThreadsNumber(files.length);
113 | const workService = new WorkService(threadNum, pluginKey, WorkerThread);
114 | //执行操作
115 | const result = files.map(
116 | async (origin): Promise<[FileType, FileType, any] | null> => {
117 | //通知进度
118 | onprogress?.({
119 | done: false,
120 | message: "正在执行图像处理...",
121 | error: null,
122 | });
123 | //执行操作
124 | let args: any = {};
125 | if (type === "encryption") {
126 | const opts = options as OptionEncryType;
127 | args = {
128 | format: opts.format || origin.file.type,
129 | quality: opts.quality || 1,
130 | };
131 | } else {
132 | const opts = options as OptionStegaType;
133 | args = {
134 | message: opts.message,
135 | repeat: opts.repeat,
136 | };
137 | }
138 | //执行操作
139 | const outputData = await workService.run<{
140 | data: FileType;
141 | payload?: any;
142 | }>(origin, key, optionName, args);
143 | //如果没有结果则返回null
144 | if (!outputData) {
145 | return null;
146 | }
147 | //通知进度
148 | const newFile = createURL4FileType(outputData.data);
149 | return [origin, newFile, outputData.payload];
150 | }
151 | );
152 | //监听result用于通知进度
153 | Promise.all(result).then((res) => {
154 | const hasEmpty = res.some((item) => !item);
155 | if (hasEmpty) {
156 | onprogress?.({
157 | done: true,
158 | message: "部分图像处理失败",
159 | error: new Error("部分图像处理失败"),
160 | });
161 | } else {
162 | onprogress?.({
163 | done: true,
164 | message: "图像处理完成",
165 | error: null,
166 | });
167 | }
168 | });
169 | return result;
170 | }
171 | }
172 |
173 | export default ImageService;
174 |
--------------------------------------------------------------------------------
/src/service/image/type.ts:
--------------------------------------------------------------------------------
1 | import { Plugin } from "@/service/plugin/type";
2 | export interface PixelBuffer {
3 | name: string;
4 | buffer: ArrayBuffer;
5 | width: number;
6 | height: number;
7 | }
8 |
9 | export interface progressStatus {
10 | done: boolean;
11 | message: string;
12 | error: Error | null;
13 | }
14 |
15 | export type PluginJson = Omit & {
16 | default: Omit;
17 | };
18 | export type ExecFuncType = (
19 | pixelBuffer: PixelBuffer,
20 | secretKey: string,
21 | optionArgs: {
22 | message?: string | undefined;
23 | repeat?: number | undefined;
24 | }
25 | ) => Promise<{
26 | data: PixelBuffer;
27 | payload?: any;
28 | }>;
29 |
--------------------------------------------------------------------------------
/src/service/image/worker.ts:
--------------------------------------------------------------------------------
1 | import { FileType } from "@/components/Upload/type";
2 | import {
3 | file2FileType,
4 | file2PixelsBuffer,
5 | pixelsBuffer2File,
6 | } from "@/utils/file";
7 | import { ExecFuncType } from "./type";
8 | //算法
9 | import * as encry_dna from "@/plugins/encryption/DNA";
10 | import * as encry_arnold from "@/plugins/encryption/Arnold";
11 | import * as encry_logistic from "@/plugins/encryption/Logistic";
12 | import * as encry_tent from "@/plugins/encryption/Tent";
13 | import * as stega_lsb from "@/plugins/steganography/LSB";
14 | import * as stega_dct from "@/plugins/steganography/DCT";
15 | const MODULE_MAP = {
16 | encry_dna,
17 | encry_arnold,
18 | encry_logistic,
19 | encry_tent,
20 | stega_lsb,
21 | stega_dct,
22 | };
23 | //缓存函数;
24 | let cachedModule: any | null = null;
25 |
26 | const handle = async (
27 | origin: FileType, //原始文件
28 | secretKey: string, //密钥
29 | funcName: string, //要执行的函数名称
30 | options: {
31 | format?: string;
32 | quality?: number;
33 | message?: string;
34 | repeat?: number;
35 | } = {}
36 | ) => {
37 | //获取文件buffer
38 | const pixelBuffer = await file2PixelsBuffer(origin.file);
39 | const { format, quality, ...optionArgs } = options;
40 | //使用缓存函数处理
41 | const execFunc: ExecFuncType = cachedModule[funcName];
42 | if (typeof execFunc !== "function") {
43 | throw new Error("未找到对应的处理函数: " + funcName);
44 | }
45 | const resultData = await execFunc(pixelBuffer, secretKey, optionArgs);
46 | //转换为文件
47 | const file = await pixelsBuffer2File(resultData.data, format, quality);
48 | //计算md5
49 | const data = await file2FileType(file, null, false, true);
50 | const payload = resultData.payload;
51 | /**
52 | * 严重注意事项:不能再worker进程中计算Blob URL
53 | * 则当worker进程结束时对应的内存会被释放
54 | * 虽然结束前渲染到页面上的图片正常显示了,但是实际上图片已经被释放了,链接不可再次访问
55 | * 结束后渲染到页面上的图片会直接404
56 | */
57 | //已加密的文件
58 | return {
59 | data,
60 | payload,
61 | };
62 | };
63 | //注册监听事件
64 | self.addEventListener(
65 | "message",
66 | async (
67 | event: MessageEvent<{
68 | args?: [FileType, string, string, any];
69 | module?: string;
70 | }>
71 | ) => {
72 | const { args, module } = event.data;
73 | if (module) {
74 | //反序列化函数并获取模块
75 | try {
76 | //获取函数字符串
77 | cachedModule = MODULE_MAP[module as keyof typeof MODULE_MAP];
78 | } catch (error) {
79 | console.error("获取模块失败: ", error);
80 | }
81 | } else if (args) {
82 | // 执行缓存函数
83 | try {
84 | const result = await handle(...args);
85 | //发送数据
86 | self.postMessage(result);
87 | } catch (error) {
88 | console.error("图像处理失败: ", error);
89 | self.postMessage(null);
90 | }
91 | }
92 | }
93 | );
94 |
--------------------------------------------------------------------------------
/src/service/plugin/index.ts:
--------------------------------------------------------------------------------
1 | import { Plugin } from "./type";
2 |
3 | class PluginService {
4 | private plugins: Plugin[] = [];
5 | private loadedModules: Record = {};
6 |
7 | public async loadPlugin(plugin: Plugin): Promise {
8 | if (this.loadedModules[plugin.name]) {
9 | throw new Error(`Plugin ${plugin.name} has already been loaded`);
10 | }
11 |
12 | //按合适的顺序插入插件
13 | const index = this.plugins.findIndex(
14 | (obj) => obj.name.localeCompare(plugin.name) > 0
15 | );
16 | if (index === -1) {
17 | this.plugins.push(plugin);
18 | } else {
19 | this.plugins.splice(index, 0, plugin);
20 | }
21 | this.loadedModules[plugin.name] = plugin;
22 | return true;
23 | }
24 |
25 | public unloadPlugin(pluginName: string): void {
26 | this.plugins = this.plugins.filter((plugin) => plugin.name !== pluginName);
27 | delete this.loadedModules[pluginName];
28 | }
29 |
30 | public getPlugins(): Plugin[] {
31 | return this.plugins;
32 | }
33 |
34 | public forEachPlugin(callback: (plugin: Plugin) => void): void {
35 | this.plugins.forEach(callback);
36 | }
37 |
38 | public getPlugin(pluginName: string): T {
39 | const module = this.loadedModules[pluginName];
40 | if (!module) {
41 | throw new Error(`Plugin ${pluginName} has not been loaded`);
42 | }
43 |
44 | const plugin = module;
45 | if (!plugin) {
46 | throw new Error(`Invalid plugin ${pluginName}`);
47 | }
48 |
49 | return plugin as T;
50 | }
51 | }
52 | export default PluginService;
53 |
--------------------------------------------------------------------------------
/src/service/plugin/type.ts:
--------------------------------------------------------------------------------
1 | //插件的元信息
2 | export type Plugin = {
3 | name: string;
4 | key: string;
5 | version: string;
6 | description: string;
7 | language: string;
8 | keyRule: {
9 | regex: string;
10 | required: boolean;
11 | message: string;
12 | };
13 | componentRule?: {
14 | [key: string]: boolean;
15 | };
16 | };
17 |
--------------------------------------------------------------------------------
/src/service/worker/index.ts:
--------------------------------------------------------------------------------
1 | import { Task } from "./type";
2 | export default class WorkService {
3 | private readonly maxWorkers: number; //最大worker数量
4 | private readonly Script: new () => Worker; //worker脚本
5 | private moduleName: string; //worker执行模块获取函数
6 | private workers: Worker[] = []; //worker列表
7 | private taskQueue: Task[] = []; //任务队列
8 | private availableWorker = 0; //可用worker数量
9 | private promiseRes: null | ((value: any) => void) = null; //worker工作完成状态
10 | public autoDestroy = true; //是否自动销毁worker
11 | public status: Promise = new Promise(
12 | (res) => (this.promiseRes = res)
13 | );
14 |
15 | //构造函数
16 | constructor(
17 | maxWorkers: number,
18 | moduleName: string,
19 | Script: new () => Worker
20 | ) {
21 | this.maxWorkers = maxWorkers;
22 | this.Script = Script;
23 | this.moduleName = moduleName;
24 | }
25 | /**
26 | * 执行任务
27 | * @param taskArgs 任务参数
28 | * @returns 任务结果
29 | */
30 | public run(...taskArgs: any[]): Promise {
31 | return new Promise((resolve, reject) => {
32 | const task: Task = { args: taskArgs, resolve, reject };
33 | this.taskQueue.push(task);
34 | //保证所有任务先入队列再执行
35 | Promise.resolve(0).then(() => this.tryStartTask());
36 | });
37 | }
38 | /**
39 | * 设置worker的工作函数
40 | * @param moduleFunc 传入的函数
41 | */
42 | public setModule(name: string) {
43 | this.moduleName = name;
44 | this.workers.forEach((worker) => {
45 | worker.postMessage({ module: this.moduleName });
46 | });
47 | }
48 | /**
49 | * 销毁所有worker
50 | */
51 | public destroy() {
52 | console.log("销毁所有worker服务");
53 | this.workers.forEach((worker) => {
54 | worker.terminate();
55 | });
56 | this.workers = [];
57 | this.taskQueue = [];
58 | this.availableWorker = 0;
59 | }
60 | /**
61 | * 尝试执行任务
62 | */
63 | private tryStartTask() {
64 | //获取worker
65 | let worker: Worker | null = null;
66 | //情况1:队列为空, 不需要取worker
67 | if (!this.taskQueue.length) {
68 | //如果当前线程是最后一个并且就代表全部任务已完成
69 | if (this.availableWorker >= this.maxWorkers) {
70 | console.log("任务完成");
71 | //设置状态为完成
72 | this.promiseRes?.("done");
73 | //如果设置了自动销毁,就销毁所有worker
74 | if (this.autoDestroy) {
75 | this.destroy();
76 | }
77 | }
78 | return;
79 | }
80 | //情况2:队列不为空, 需要取worker
81 | //如果worker未达到最大数量,创建新的worker
82 | if (this.workers.length < this.maxWorkers) {
83 | console.log("new Worker");
84 | worker = new this.Script();
85 | this.workers.push(worker);
86 | worker.postMessage({ module: this.moduleName });
87 | }
88 | //否则,并且如果worker已达到最大数量,判断是否有可用worker
89 | else if (this.availableWorker > 0) {
90 | this.availableWorker--;
91 | worker = this.workers.find(({ onmessage }) => !onmessage)!;
92 | }
93 | //否则,判断为没有可用worker,直接返回
94 | else {
95 | return;
96 | }
97 | //从任务队列中取出一个任务
98 | const task = this.taskQueue.shift();
99 | //如果任务已经被其他进程抢走,直接返回
100 | if (!task) {
101 | return;
102 | }
103 | //添加任务完成回调
104 | worker.onmessage = (event) => {
105 | worker!.onmessage = null;
106 | task.resolve(event.data);
107 | this.availableWorker++;
108 | this.tryStartTask();
109 | };
110 | worker.onerror = (event) => {
111 | task.reject(event);
112 | };
113 | worker.postMessage({ args: task.args });
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/service/worker/type.ts:
--------------------------------------------------------------------------------
1 | export type Task = {
2 | args: any[];
3 | resolve: (result: any) => void;
4 | reject: (error: any) => void;
5 | };
6 |
--------------------------------------------------------------------------------
/src/utils/dna.ts:
--------------------------------------------------------------------------------
1 | export enum DNACodeEnum {
2 | A = "A",
3 | T = "T",
4 | C = "C",
5 | G = "G",
6 | }
7 | export enum RuleEnum {
8 | rule1 = 0,
9 | rule2 = 1,
10 | rule3 = 2,
11 | rule4 = 3,
12 | rule5 = 4,
13 | rule6 = 5,
14 | rule7 = 6,
15 | rule8 = 7,
16 | }
17 | export enum Bit2Enum {
18 | bit0 = 0b00,
19 | bit1 = 0b01,
20 | bit2 = 0b10,
21 | bit3 = 0b11,
22 | }
23 | export type DNAByte = [DNACodeEnum, DNACodeEnum, DNACodeEnum, DNACodeEnum];
24 | export type Bit2Byte = [Bit2Enum, Bit2Enum, Bit2Enum, Bit2Enum];
25 | //常量
26 | const MAP_DNAENCODE: {
27 | [key: number]: { [key: number]: DNACodeEnum[keyof DNACodeEnum] };
28 | } = {
29 | 0: { 0: "A", 1: "C", 2: "G", 3: "T" },
30 | 1: { 0: "A", 1: "G", 2: "C", 3: "T" },
31 | 2: { 0: "C", 1: "A", 2: "T", 3: "G" },
32 | 3: { 0: "C", 1: "T", 2: "A", 3: "G" },
33 | 4: { 0: "G", 1: "A", 2: "T", 3: "C" },
34 | 5: { 0: "G", 1: "T", 2: "A", 3: "C" },
35 | 6: { 0: "T", 1: "C", 2: "G", 3: "A" },
36 | 7: { 0: "T", 1: "G", 2: "C", 3: "A" },
37 | };
38 | const MAP_DNADECODE: { [key: number]: { [key: string]: Bit2Enum } } = {
39 | 0: { A: 0, C: 1, G: 2, T: 3 },
40 | 1: { A: 0, G: 1, C: 2, T: 3 },
41 | 2: { C: 0, A: 1, T: 2, G: 3 },
42 | 3: { C: 0, T: 1, A: 2, G: 3 },
43 | 4: { G: 0, A: 1, T: 2, C: 3 },
44 | 5: { G: 0, T: 1, A: 2, C: 3 },
45 | 6: { T: 0, C: 1, G: 2, A: 3 },
46 | 7: { T: 0, G: 1, C: 2, A: 3 },
47 | };
48 | const TABLE_ADD4RULE_1: {
49 | [key: string]: { [key: string]: DNACodeEnum[keyof DNACodeEnum] };
50 | } = {
51 | A: { A: "A", T: "T", C: "C", G: "G" },
52 | T: { A: "T", T: "G", C: "A", G: "C" },
53 | C: { A: "C", T: "A", C: "G", G: "T" },
54 | G: { A: "G", T: "C", C: "T", G: "A" },
55 | };
56 | const TABLE_SUB4RULE_1: {
57 | [key: string]: { [key: string]: DNACodeEnum[keyof DNACodeEnum] };
58 | } = {
59 | A: { A: "A", T: "C", C: "T", G: "G" },
60 | T: { A: "T", T: "A", C: "G", G: "C" },
61 | C: { A: "C", T: "G", C: "A", G: "T" },
62 | G: { A: "G", T: "T", C: "C", G: "A" },
63 | };
64 | const TABLE_XOR4RULE_1: {
65 | [key: string]: { [key: string]: DNACodeEnum[keyof DNACodeEnum] };
66 | } = {
67 | A: { A: "A", T: "T", C: "C", G: "G" },
68 | T: { A: "T", T: "A", C: "G", G: "C" },
69 | C: { A: "C", T: "G", C: "A", G: "T" },
70 | G: { A: "G", T: "C", C: "T", G: "A" },
71 | };
72 | const TABLE_XNOR4RULE_1: {
73 | [key: string]: { [key: string]: DNACodeEnum[keyof DNACodeEnum] };
74 | } = {
75 | A: { A: "T", T: "A", C: "G", G: "C" },
76 | T: { A: "A", T: "T", C: "C", G: "G" },
77 | C: { A: "G", T: "C", C: "T", G: "A" },
78 | G: { A: "C", T: "G", C: "A", G: "T" },
79 | };
80 | /**
81 | * 将字节转为DNA编码
82 | * @param byte 输入的字节
83 | * @param rule 编码规则
84 | */
85 | export const byte2DNAByte = (byte: number, rule: RuleEnum) => {
86 | return [
87 | dnaEncode((byte >> 6) & 0b00000011, rule),
88 | dnaEncode((byte >> 4) & 0b00000011, rule),
89 | dnaEncode((byte >> 2) & 0b00000011, rule),
90 | dnaEncode((byte >> 0) & 0b00000011, rule),
91 | ] as DNAByte;
92 | };
93 | /**
94 | * 将DNA编码转为字节
95 | * @param code 输入的DNA编码
96 | * @param rule 解码规则
97 | */
98 | export const dnaByte2Byte = (code: DNAByte, rule: RuleEnum) => {
99 | return (
100 | (dnaDecode(code[0], rule) << 6) |
101 | (dnaDecode(code[1], rule) << 4) |
102 | (dnaDecode(code[2], rule) << 2) |
103 | (dnaDecode(code[3], rule) << 0)
104 | );
105 | };
106 | /**
107 | * DNA编码
108 | * @param num 输入的数值
109 | * @param rule 编码规则
110 | * @returns 编码后的核苷酸
111 | */
112 | export const dnaEncode = (num: Bit2Enum, rule: number) => {
113 | return MAP_DNAENCODE[rule][num];
114 | };
115 | /**
116 | * DNA解码
117 | * @param base 输入的碱基
118 | * @param rule 解码规则
119 | * @returns 解码后的数值
120 | */
121 | export const dnaDecode = (base: DNACodeEnum, rule: number) => {
122 | return MAP_DNADECODE[rule][base];
123 | };
124 | /**
125 | * 规则1对应的加法运算
126 | * @param row 输入的碱基
127 | * @param col 输入的碱基
128 | * @returns 运算后的碱基
129 | */
130 | export const add4Rule1 = (row: DNACodeEnum, col: DNACodeEnum) => {
131 | return TABLE_ADD4RULE_1[row][col];
132 | };
133 | /**
134 | * 规则1对应的减法运算
135 | * @param row 输入的碱基
136 | * @param col 输入的碱基
137 | * @returns 运算后的碱基
138 | */
139 | export const sub4Rule1 = (row: DNACodeEnum, col: DNACodeEnum) => {
140 | return TABLE_SUB4RULE_1[row][col];
141 | };
142 | /**
143 | * 规则1对应的异或运算
144 | * @param row 输入的碱基
145 | * @param col 输入的碱基
146 | * @returns 运算后的碱基
147 | */
148 | export const xor4Rule1 = (row: DNACodeEnum, col: DNACodeEnum) => {
149 | return TABLE_XOR4RULE_1[row][col];
150 | };
151 | /**
152 | * XNOR运算
153 | * @param row 输入的碱基1
154 | * @param col 输入的碱基2
155 | * @returns 运算后的碱基
156 | */
157 | export const xnor4Rule1 = (row: DNACodeEnum, col: DNACodeEnum) => {
158 | return TABLE_XNOR4RULE_1[row][col];
159 | };
160 |
--------------------------------------------------------------------------------
/src/utils/file.ts:
--------------------------------------------------------------------------------
1 | import { PixelBuffer } from "@/service/image/type";
2 | import SparkMD5 from "spark-md5";
3 | import { getNewFileName } from "./string";
4 | import Pica from "pica";
5 | import { FileType } from "@/components/Upload/type";
6 | import produce from "immer";
7 |
8 | /**
9 | * 计算文件的 MD5 值
10 | * @param file 文件对象
11 | * @param onProgress 计算进度回调函数
12 | * @returns Promise MD5 值
13 | */
14 | export function calculateMD5(
15 | file: File,
16 | onProgress?: (progress: number) => void
17 | ): Promise {
18 | const fileSize = file.size;
19 | const chunkSize = 1024 * 1024 * 10; // 每片 10MB
20 |
21 | if (fileSize <= chunkSize) {
22 | return new Promise((resolve) => {
23 | const reader = new FileReader();
24 | reader.readAsArrayBuffer(file);
25 | reader.onload = (e) => {
26 | const spark = new SparkMD5.ArrayBuffer();
27 | spark.append(e.target?.result as ArrayBuffer);
28 | const md5 = spark.end();
29 | resolve(md5);
30 | };
31 | });
32 | }
33 |
34 | return new Promise((resolve) => {
35 | const chunks = Math.ceil(fileSize / chunkSize);
36 | let currentChunk = 0;
37 | let currentPosition = 0;
38 | const spark = new SparkMD5.ArrayBuffer();
39 |
40 | const fileReader = new FileReader();
41 |
42 | fileReader.onload = function (e) {
43 | spark.append(e.target?.result as ArrayBuffer);
44 |
45 | currentPosition += chunkSize;
46 | currentChunk++;
47 |
48 | if (currentChunk < chunks) {
49 | loadNext();
50 | } else {
51 | const md5 = spark.end();
52 | resolve(md5);
53 | }
54 |
55 | if (onProgress) {
56 | const progress = Math.min((currentPosition / fileSize) * 100, 100);
57 | onProgress(progress);
58 | }
59 | };
60 |
61 | function loadNext() {
62 | const start = currentPosition;
63 | const end = Math.min(start + chunkSize, fileSize);
64 | fileReader.readAsArrayBuffer(file.slice(start, end));
65 | }
66 |
67 | loadNext();
68 | });
69 | }
70 | /**
71 | *
72 | * @param file 任意格式的图像文件
73 | * @param quality 图像压缩质量,取值范围为0-1,默认不压缩
74 | * @returns 图像文件的ArrayBuffer像素数据
75 | */
76 | export async function file2PixelsBuffer(
77 | file: File,
78 | quality = 1
79 | ): Promise {
80 | const img = await createImageBitmap(file);
81 | const canvas = new OffscreenCanvas(img.width, img.height);
82 | const ctx = canvas.getContext("2d") as OffscreenCanvasRenderingContext2D;
83 | ctx.drawImage(img, 0, 0);
84 | //压缩图片
85 | if (quality < 1) {
86 | const blob = await (canvas as any).convertToBlob({
87 | type: "image/jpeg",
88 | quality,
89 | });
90 | const lossyImage = await createImageBitmap(blob);
91 | //清空画布
92 | ctx.clearRect(0, 0, canvas.width, canvas.height);
93 | ctx.drawImage(lossyImage, 0, 0);
94 | }
95 | const pixelData = ctx.getImageData(0, 0, img.width, img.height).data.buffer;
96 | return {
97 | name: file.name,
98 | buffer: pixelData,
99 | width: img.width,
100 | height: img.height,
101 | };
102 | }
103 | /**
104 | *
105 | * @param pixels 图像像素数据
106 | * @param type 要生成图片的类型
107 | * @returns 图像文件
108 | */
109 | export async function pixelsBuffer2File(
110 | pixelBuffer: PixelBuffer,
111 | type = "image/png",
112 | quality = 1
113 | ): Promise {
114 | const { buffer, width, height, name } = pixelBuffer;
115 | const imageData = new ImageData(new Uint8ClampedArray(buffer), width, height);
116 | const offscreenCanvas = new OffscreenCanvas(width, height);
117 | const ctx = offscreenCanvas.getContext(
118 | "2d"
119 | ) as OffscreenCanvasRenderingContext2D;
120 | ctx.putImageData(imageData, 0, 0);
121 | //type对于所有格式并非全部有效,依据浏览器支持情况而定
122 | const blob = await (offscreenCanvas as any).convertToBlob({
123 | type,
124 | quality,
125 | });
126 | //处理文件名
127 | const newName = getNewFileName(name, blob.type);
128 | return new File([blob], `encrypted-${newName}`, { type: blob.type });
129 | }
130 | /**
131 | *
132 | * @param file File格式图像文件
133 | * @returns Image图像对象
134 | */
135 | export const file2Image = (file: File): Promise => {
136 | return new Promise((resolve, reject) => {
137 | const img = new Image();
138 | img.src = URL.createObjectURL(file);
139 | img.onload = () => {
140 | URL.revokeObjectURL(img.src);
141 | resolve(img);
142 | };
143 | img.onerror = () => {
144 | URL.revokeObjectURL(img.src);
145 | reject(new Error("Failed to load image"));
146 | };
147 | });
148 | };
149 | /**
150 | * @param origin File格式图像文件
151 | * @param current File格式图像文件
152 | * @returns 压缩率
153 | */
154 | export const getCompressionRate = (origin: File, current: File) => {
155 | return Math.round((current.size / origin.size) * 10000) / 100;
156 | };
157 |
158 | /**
159 | * 将长方形图像填充为正方形,采用尾部填充的方式。
160 | * @param data 要填充的像素数据
161 | * @returns 填充后的像素数据
162 | */
163 | export const padImageToSquare = (data: PixelBuffer): PixelBuffer => {
164 | const { width, height, buffer, name } = data;
165 | const maxDim = Math.max(width, height);
166 | if (width === height) {
167 | // 已经是正方形,无需填充
168 | return data;
169 | } else if (width > height) {
170 | // 宽度大于高度,向下填充
171 | const paddedBuffer = new Uint8ClampedArray(maxDim ** 2 * 4);
172 | paddedBuffer.set(new Uint8ClampedArray(buffer));
173 | return {
174 | name,
175 | width: maxDim,
176 | height: maxDim,
177 | buffer: paddedBuffer.buffer,
178 | };
179 | } else {
180 | // 高度大于宽度,向右填充
181 | const paddedBuffer = new Uint8ClampedArray(maxDim ** 2 * 4);
182 | const rowSize = width * 4;
183 | for (let i = 0; i < height; i++) {
184 | const offset = i * rowSize;
185 | paddedBuffer.set(
186 | new Uint8ClampedArray(buffer, offset, rowSize),
187 | offset + i * (maxDim - width) * 4
188 | );
189 | }
190 | return {
191 | name,
192 | width: maxDim,
193 | height: maxDim,
194 | buffer: paddedBuffer.buffer,
195 | };
196 | }
197 | };
198 | /**
199 | * 将正方形图像还原为长方形,去除尾部填充的部分。
200 | * @param data 要还原的像素数据
201 | * @returns 还原后的像素数据
202 | */
203 | export const restoreImageFromSquare = (data: PixelBuffer): PixelBuffer => {
204 | const { width, height, buffer, name } = data;
205 | if (width !== height) {
206 | // 不是正方形,无需还原
207 | return data;
208 | }
209 |
210 | //从最后一行倒序遍历,若该行55%为零,则认为该行为填充行
211 | let paddingBottom = 0;
212 | let paddingRight = 0;
213 | for (let i = height - 1; i >= 0; i--) {
214 | const rowSize = width * 4;
215 | const offset = i * rowSize;
216 | const row = new Uint8ClampedArray(buffer, offset, rowSize);
217 | let blockLength = 0;
218 | let alphaLength = 0;
219 | row.forEach((pixels, index) => {
220 | if (pixels < 30) {
221 | if (index % 3 !== 0) {
222 | blockLength++;
223 | } else {
224 | alphaLength++;
225 | }
226 | }
227 | });
228 | if (
229 | (alphaLength * 4) / row.length > 0.55 ||
230 | (blockLength * 4) / (row.length * 3) > 0.55
231 | ) {
232 | paddingBottom++;
233 | } else {
234 | break;
235 | }
236 | }
237 | if (paddingBottom > 0) {
238 | //去除填充行
239 | const newheight = height - paddingBottom;
240 | const restoredBuffer = new Uint8ClampedArray(newheight * width * 4);
241 | restoredBuffer.set(new Uint8ClampedArray(buffer, 0, newheight * width * 4));
242 | return {
243 | name,
244 | width,
245 | height: newheight,
246 | buffer: restoredBuffer.buffer,
247 | };
248 | }
249 | //从右侧开始,若该列55%为零,则认为该列为填充列
250 | for (let i = width - 1; i >= 0; i--) {
251 | const column = new Uint8ClampedArray(height * 4);
252 | for (let j = 0; j < height; j++) {
253 | column.set(
254 | new Uint8ClampedArray(buffer, j * width * 4 + i * 4, 4),
255 | j * 4
256 | );
257 | }
258 | let blockLength = 0;
259 | let alphaLength = 0;
260 | column.forEach((pixels, index) => {
261 | if (pixels < 30) {
262 | if (index % 3 !== 0) {
263 | blockLength++;
264 | } else {
265 | alphaLength++;
266 | }
267 | }
268 | });
269 | if (
270 | (alphaLength * 4) / column.length > 0.55 ||
271 | (blockLength * 4) / (column.length * 3) > 0.55
272 | ) {
273 | paddingRight++;
274 | } else {
275 | break;
276 | }
277 | }
278 | //去除填充列
279 | const restoredBuffer = new Uint8ClampedArray(
280 | height * (width - paddingRight) * 4
281 | );
282 | for (let i = 0; i < height; i++) {
283 | restoredBuffer.set(
284 | new Uint8ClampedArray(buffer, i * width * 4, (width - paddingRight) * 4),
285 | i * (width - paddingRight) * 4
286 | );
287 | }
288 | return {
289 | name,
290 | width: width - paddingRight,
291 | height,
292 | buffer: restoredBuffer.buffer,
293 | };
294 | };
295 | /**
296 | * 获取图像缩略图
297 | * @param file 图像文件
298 | * @param targetWidth 目标宽度
299 | * @param targetHeight 目标高度
300 | * @param isWorker 是否在web worker中运行
301 | * @returns 缩略图文件
302 | */
303 | export const getThumbnail = async (
304 | file: File,
305 | targetWidth: number,
306 | targetHeight: number,
307 | isWorker?: boolean
308 | ): Promise => {
309 | try {
310 | const pica = new Pica({
311 | createCanvas: (width, height) =>
312 | new OffscreenCanvas(width, height) as any,
313 | });
314 | //将文件转换为图像数据, web worker不能访问Image, 但是file2Image效率更快一点
315 | const imageData = isWorker
316 | ? await createImageBitmap(file)
317 | : await file2Image(file);
318 | //计算缩放比例
319 | const scale = Math.min(
320 | targetWidth / imageData.width,
321 | targetHeight / imageData.height
322 | );
323 | //设置缩略图大小
324 | const destCanvas = new OffscreenCanvas(
325 | imageData.width * scale,
326 | imageData.height * scale
327 | );
328 | //生成缩略图
329 | await pica.resize(imageData, destCanvas as any);
330 | //const resizedBlob = await pica.toBlob(result, "image/png"); //火狐浏览器会阻止OffscreenCanvas的toBlob方法
331 | const resizedBlob = await (destCanvas as any).convertToBlob({
332 | type: "image/png",
333 | });
334 | //返回缩略图文件
335 | return new File([resizedBlob], file.name, {
336 | type: resizedBlob.type,
337 | });
338 | } catch (error) {
339 | console.error("生成缩略图失败: ", error);
340 | return file;
341 | }
342 | };
343 | /**
344 | *
345 | * @param file 文件
346 | * @param fileMd5 文件md5
347 | * @param isCreateURL 是否创建url
348 | * @param thumbnailWidth 缩略图宽度
349 | * @param thumbnailHeight 缩略图高度
350 | * @returns Promise 复合类型文件
351 | */
352 | export const file2FileType = async (
353 | file: File,
354 | fileMd5?: string | null | false,
355 | isCreateURL = true,
356 | isWorker = false,
357 | thumbnailWidth = 128,
358 | thumbnailHeight = 128
359 | ): Promise => {
360 | //获取缩略图
361 | const thumFile = await getThumbnail(
362 | file,
363 | thumbnailWidth,
364 | thumbnailHeight,
365 | isWorker
366 | );
367 | //计算文件md5
368 | const md5 = fileMd5 || (await calculateMD5(file));
369 | //计算图像的src
370 | const src = isCreateURL ? URL.createObjectURL(file) : "";
371 | const thumSrc = isCreateURL ? URL.createObjectURL(thumFile) : "";
372 | //构建缩略图对象
373 | const thumbnail = {
374 | file: thumFile,
375 | src: thumSrc,
376 | };
377 | return {
378 | file,
379 | src,
380 | md5,
381 | thumbnail,
382 | };
383 | };
384 | /**
385 | * 为FileType对象创建URL
386 | * @param fileType 可能未携带src或者src为空的FileType对象
387 | * @returns 带有完整src的FileType对象
388 | */
389 | export const createURL4FileType = (fileType: FileType): FileType => {
390 | return produce((draft) => {
391 | if (!draft.src) {
392 | draft.src = URL.createObjectURL(draft.file);
393 | }
394 | if (!draft.thumbnail.src) {
395 | draft.thumbnail.src = URL.createObjectURL(draft.thumbnail.file);
396 | }
397 | })(fileType);
398 | };
399 |
--------------------------------------------------------------------------------
/src/utils/function.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 序列化函数
3 | * @param func 函数
4 | * @returns ArrayBuffer
5 | */
6 | export const serializeFunction = (
7 | func: (...args: any[]) => any
8 | ): ArrayBuffer => {
9 | const funcString = func.toString();
10 | const encoder = new TextEncoder();
11 | const funcData = encoder.encode(funcString);
12 | return funcData.buffer;
13 | };
14 | /**
15 | * 反序列化函数, 但不支持import.meta
16 | * @param buffer ArrayBuffer
17 | * @returns 函数字符串
18 | */
19 | export const deserializeFunction = (buffer: ArrayBuffer): string => {
20 | const data = new Uint8Array(buffer);
21 | const decoder = new TextDecoder("utf-8");
22 | return decoder.decode(data);
23 | };
24 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | import { twoThirds } from "./number";
2 | //这里存放更偏向业务的工具函数
3 | /**
4 | * 获取线程数
5 | */
6 | export const getThreadsNumber = (num: number) => {
7 | if (num >= 1) {
8 | return Math.max(
9 | Math.min(Math.ceil(num / 3), twoThirds(navigator.hardwareConcurrency)),
10 | 1
11 | );
12 | }
13 | return 1;
14 | };
15 | /**
16 | * 判断是否是基于Chromium内核的浏览器
17 | * @returns boolean
18 | */
19 | export const isChromiumBased = (): boolean => {
20 | return (
21 | /(Chrome|Chromium|Edg)/i.test(window.navigator.userAgent) ||
22 | (window as any).chrome
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/src/utils/number.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * @param value 输入的数值
4 | * @returns 计算输入的百分比,输入数值无小数或者最多一位小数
5 | */
6 | export const decimalToPercentage = (value: number): number => {
7 | if (isNaN(value)) {
8 | return 0;
9 | }
10 | const percentage = Math.round(value * 1000) / 10;
11 | return Math.max(0, Math.min(100, percentage));
12 | };
13 | /**
14 | *
15 | * @param num 输入的数值
16 | * @returns 计算输入的2/3的值
17 | */
18 | export const twoThirds = (num: number): number => {
19 | return Math.floor((num * 2) / 3);
20 | };
21 | /**
22 | * 将B转换为KB,MB,GB,TB,PB
23 | * @param size 输入的大小
24 | * @returns 返回转换后的大小
25 | */
26 | const SIZE_POWER_MAP = {
27 | B: 0,
28 | KB: 1,
29 | MB: 2,
30 | GB: 3,
31 | TB: 4,
32 | PB: 5,
33 | EB: 6,
34 | ZB: 7,
35 | YB: 8,
36 | };
37 | export const formatSize = (
38 | size: number,
39 | type: "KB" | "MB" | "GB" | "TB" | "PB" | "EB" | "ZB" | "YB"
40 | ): string => {
41 | return `${(size / Math.pow(1024, SIZE_POWER_MAP[type])).toFixed(1)} ${type}`;
42 | };
43 |
--------------------------------------------------------------------------------
/src/utils/object.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * @param objA 对象
4 | * @param objB 对象
5 | * @returns 两对象的浅比较结果
6 | */
7 | export const shallowEqual = (objA: any, objB: any) => {
8 | if (objA === objB) {
9 | return true;
10 | }
11 |
12 | if (
13 | typeof objA !== "object" ||
14 | objA === null ||
15 | typeof objB !== "object" ||
16 | objB === null
17 | ) {
18 | return false;
19 | }
20 |
21 | const keysA = Object.keys(objA);
22 | const keysB = Object.keys(objB);
23 |
24 | if (keysA.length !== keysB.length) {
25 | return false;
26 | }
27 |
28 | for (let i = 0; i < keysA.length; i++) {
29 | const key = keysA[i];
30 | if (objA[key] !== objB[key]) {
31 | return false;
32 | }
33 | }
34 |
35 | return true;
36 | };
37 |
--------------------------------------------------------------------------------
/src/utils/string.ts:
--------------------------------------------------------------------------------
1 | import SparkMD5 from "spark-md5";
2 |
3 | /**
4 | *
5 | * @param str 字符串
6 | * @returns 首字符大写的字符串
7 | */
8 | export const capitalizeFirstLetter = (str: string) => {
9 | if (typeof str !== "string" || !str.trim()) {
10 | return "";
11 | }
12 | return str.trim().charAt(0).toUpperCase() + str.slice(1);
13 | };
14 | /**
15 | * 将十六进制字符串转变为数值形式
16 | * @param str 密钥字符串
17 | * @param normalize 是否归一化
18 | * @param md5 是否使用md5算法处理输入字符串
19 | * @param sliceLength 截取长度
20 | * @returns 数值
21 | */
22 | export const str2Num = (
23 | str: string,
24 | normalize = false,
25 | md5 = false,
26 | sliceLength = 13
27 | ): number => {
28 | // 截取长度
29 | const sliceLen = sliceLength > 13 ? 13 : sliceLength;
30 | // 使用SparkMD5算法对密钥进行哈希处理
31 | const hash = md5 ? SparkMD5.hash(str) : str;
32 | // 取哈希值前13位作为十六进制字符串
33 | const hex = hash.slice(0, sliceLen);
34 | // 将十六进制字符串转换为十进制数
35 | const num = parseInt(hex, 16);
36 | // 如果需要归一化,则将数值限制在0-1之间
37 | if (normalize) {
38 | return num / parseInt("f".repeat(sliceLen), 16);
39 | }
40 | return num;
41 | };
42 |
43 | export const getNewFileName = (filename: string, MIME: string): string => {
44 | // 获取文件名(不包括扩展名)和扩展名
45 | const fileParts = filename.split(".");
46 | if (fileParts.length < 2) {
47 | return filename;
48 | }
49 | const nameWithoutExtension = fileParts.slice(0, -1).join(".");
50 | // 根据 MIME 类型设置新的扩展名
51 | let newExtension: string;
52 |
53 | switch (MIME) {
54 | case "image/png":
55 | newExtension = "png";
56 | break;
57 | case "image/jpeg":
58 | newExtension = "jpg";
59 | break;
60 | case "image/bmp":
61 | newExtension = "bmp";
62 | break;
63 | case "image/webp":
64 | newExtension = "webp";
65 | break;
66 | default:
67 | newExtension = fileParts[fileParts.length - 1];
68 | }
69 |
70 | // 返回新的文件名
71 | return `${nameWithoutExtension}.${newExtension}`;
72 | };
73 |
--------------------------------------------------------------------------------
/src/utils/webgl.ts:
--------------------------------------------------------------------------------
1 | import { PixelBuffer } from "@/service/image/type";
2 | /**
3 | * 创建 WebGL 上下文
4 | * @param width 宽度
5 | * @param height 高度
6 | * @param version 版本
7 | */
8 | export const createWebGLContext = (
9 | width: number,
10 | height: number,
11 | type: OffscreenRenderingContextId
12 | ): T => {
13 | const canvas = new OffscreenCanvas(width, height);
14 | canvas.addEventListener("webglcontextlost", function (e) {
15 | console.log(e, 111);
16 | });
17 | const gl = canvas.getContext(type, {
18 | preserveDrawingBuffer: true, //阻止保留绘制缓冲区
19 | }) as T;
20 | //gl不能为null
21 | if (!gl) {
22 | throw new Error("WebGL Context Create Error");
23 | }
24 | return gl;
25 | };
26 |
27 | /**
28 | * 编译 WebGL 着色器。
29 | * @param gl WebGLRenderingContext 对象。
30 | * @param type 着色器类型,可为 gl.VERTEX_SHADER 或 gl.FRAGMENT_SHADER。
31 | * @param source 着色器源码。
32 | * @returns 编译后的着色器对象。
33 | */
34 | export const compileShader = (
35 | gl: WebGLRenderingContext,
36 | type: number,
37 | source: string
38 | ): WebGLShader => {
39 | const shader = gl.createShader(type);
40 | //shader不能为null
41 | if (!shader) {
42 | throw new Error("WebGL Shader Create Error");
43 | }
44 | gl.shaderSource(shader, source);
45 | gl.compileShader(shader);
46 | const message = gl.getShaderInfoLog(shader);
47 | if (message && message.length > 0) {
48 | throw message;
49 | }
50 | return shader;
51 | };
52 |
53 | /**
54 | * 链接 WebGL 着色器程序。
55 | * @param gl WebGLRenderingContext 对象。
56 | * @param shaderProgram 要链接的着色器程序对象。
57 | * @param shaders 要链接的着色器对象列表。
58 | * @returns 链接后的着色器程序对象。
59 | */
60 | export const linkShader = (
61 | gl: WebGLRenderingContext,
62 | shaderProgram: WebGLProgram,
63 | ...shaders: WebGLShader[]
64 | ): WebGLProgram => {
65 | for (const shader of shaders) {
66 | gl.attachShader(shaderProgram, shader);
67 | }
68 | gl.linkProgram(shaderProgram);
69 | const message = gl.getProgramInfoLog(shaderProgram);
70 | if (message && message.length > 0) {
71 | throw message;
72 | }
73 | for (const shader of shaders) {
74 | gl.deleteShader(shader);
75 | }
76 | return shaderProgram;
77 | };
78 |
79 | /**
80 | * 创建 WebGL 着色器程序对象。
81 | * @param gl WebGLRenderingContext 对象。
82 | * @param vertexShaderSource 顶点着色器源码。
83 | * @param fragmentShaderSource 片元着色器源码。
84 | * @returns 着色器程序对象。
85 | */
86 | export const createWebGLProgram = (
87 | gl: WebGLRenderingContext,
88 | shadersType: number[],
89 | shadersSource: string[]
90 | ): WebGLProgram => {
91 | if (shadersType.length !== shadersSource.length) {
92 | throw new Error("shadersType length not equal shadersSource length");
93 | }
94 | const shaders = [];
95 | for (let i = 0; i < shadersSource.length; i++) {
96 | const shader = compileShader(gl, shadersType[i], shadersSource[i]);
97 | shaders.push(shader);
98 | }
99 | const shaderProgram = gl.createProgram();
100 | //shaderProgram不能为null
101 | if (!shaderProgram) {
102 | throw new Error("WebGL Program Create Error");
103 | }
104 | return linkShader(gl, shaderProgram, ...shaders);
105 | };
106 |
107 | /**
108 | * 创建纹理
109 | * @param gl WebGLRenderingContext 对象。
110 | * @param pixelData 像素数据。
111 | * @returns 纹理对象。
112 | */
113 | export const createTexture = (
114 | gl: WebGLRenderingContext,
115 | pixelData: PixelBuffer
116 | ) => {
117 | const { width, height, buffer } = pixelData;
118 | const inputBuffer = new Uint8Array(buffer);
119 | //生成纹理
120 | const texture = gl.createTexture();
121 | //texture不能为null
122 | if (!texture) {
123 | throw new Error("WebGL Texture Create Error");
124 | }
125 | //绑定纹理对象
126 | gl.bindTexture(gl.TEXTURE_2D, texture);
127 | //贴图
128 | gl.texImage2D(
129 | gl.TEXTURE_2D,
130 | 0,
131 | gl.RGBA,
132 | width,
133 | height,
134 | 0,
135 | gl.RGBA,
136 | gl.UNSIGNED_BYTE,
137 | inputBuffer
138 | );
139 |
140 | //纹理过滤
141 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
142 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
143 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
144 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
145 | //生成多级渐远纹理
146 | gl.generateMipmap(gl.TEXTURE_2D);
147 | //解绑纹理对象
148 | gl.bindTexture(gl.TEXTURE_2D, null);
149 | return texture;
150 | };
151 | /**
152 | * 创建顶点缓冲区
153 | *
154 | */
155 | export const createVertexBuffer = (
156 | gl: WebGLRenderingContext,
157 | target: number,
158 | data: ArrayBuffer | ArrayBufferView | SharedArrayBuffer,
159 | usage: number
160 | ) => {
161 | const positionBuffer = gl.createBuffer();
162 | gl.bindBuffer(target, positionBuffer);
163 | gl.bufferData(target, data, usage);
164 | gl.bindBuffer(gl.ARRAY_BUFFER, null);
165 | return positionBuffer;
166 | };
167 | /**
168 | * 通过WebGL对图像进行处理
169 | * @param data 输入图像数据
170 | * @param fragmentShaderSource 片元着色器源码
171 | * @param process 中间处理函数
172 | */
173 | export const processImageByWebGL2 = (
174 | data: PixelBuffer,
175 | fragmentShaderSource: string,
176 | process?: (gl: WebGL2RenderingContext, program: WebGLProgram) => any
177 | ): PixelBuffer => {
178 | // 定义顶点着色器代码
179 | const vertexShaderSource = `#version 300 es
180 | precision highp float;
181 | layout (location = 0) in vec2 a_position;
182 | out vec2 v_texcoord;
183 | void main() {
184 | gl_Position = vec4(a_position, 0, 1);
185 | v_texcoord = a_position * 0.5 + 0.5;// 将顶点坐标转换为纹理坐标, 方便纹理采样
186 | }
187 | `;
188 | // 提取输入数据
189 | const { name, buffer, width, height } = data;
190 | // 创建一个新的 ArrayBuffer 用于存储输出数据
191 | const outputBuffer = new ArrayBuffer(buffer.byteLength);
192 | const outputData = new Uint8Array(outputBuffer);
193 | // 创建 WebGL 上下文
194 | const gl = createWebGLContext(
195 | width,
196 | height,
197 | "webgl2"
198 | );
199 | // 创建 WebGL 程序
200 | const program = createWebGLProgram(
201 | gl,
202 | [gl.VERTEX_SHADER, gl.FRAGMENT_SHADER],
203 | [vertexShaderSource, fragmentShaderSource]
204 | );
205 | // 设置 WebGL 视口
206 | gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
207 | // 创建顶点缓冲区
208 | const positionBuffer = createVertexBuffer(
209 | gl,
210 | gl.ARRAY_BUFFER,
211 | new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
212 | gl.STATIC_DRAW
213 | );
214 | // 创建纹理
215 | const texture = createTexture(gl, data);
216 | // 使用 WebGL 进行渲染
217 | gl.useProgram(program);
218 | // 绑定顶点缓冲区
219 | gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
220 | gl.enableVertexAttribArray(0);
221 | gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 2 * 4, 0);
222 | // 在默认的激活纹理单元上绑定纹理, 并反转 Y 轴
223 | gl.bindTexture(gl.TEXTURE_2D, texture);
224 | gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
225 | // 执行自定义处理
226 | process?.(gl, program);
227 | // 绘制
228 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
229 | // 从缓冲区中读取输出数据
230 | gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, outputData);
231 | // 解绑对象
232 | gl.bindTexture(gl.TEXTURE_2D, null);
233 | gl.bindBuffer(gl.ARRAY_BUFFER, null);
234 | // 删除 WebGL 程序
235 | gl.deleteProgram(program);
236 | return {
237 | name,
238 | buffer: outputBuffer,
239 | width,
240 | height,
241 | };
242 | };
243 |
--------------------------------------------------------------------------------
/src/utils/zip.ts:
--------------------------------------------------------------------------------
1 | import JSZip from "jszip";
2 | import { saveAs } from "file-saver";
3 | const MAX_ZIP_SIZE = 700 * 1024 * 1024; // 700MB,最大压缩包大小
4 |
5 | /**
6 | * 创建保存压缩包
7 | * @param files 文件列表
8 | * @param maxSize 最大压缩包大小
9 | */
10 | export const multipleFileDownload = async (
11 | files: File[],
12 | zipName = "encrypted-images",
13 | maxSize = MAX_ZIP_SIZE
14 | ) => {
15 | let currentZipSize = 0; // 当前压缩包大小
16 | let currentZipIndex = 0; // 当前压缩包索引
17 | let zip = new JSZip();
18 | for (let i = 0; i < files.length; i++) {
19 | const file = files[i];
20 | //如果当前文件大小超过最大值或者是单文件则直接保存该文件
21 | if (file.size > maxSize || files.length === 1) {
22 | saveAs(file, file.name);
23 | continue;
24 | }
25 | // 如果当前压缩包大小超过最大值,或者当前文件大小超过最大值,则生成并保存该压缩包,然后创建一个新的压缩包
26 | if (currentZipSize + file.size > maxSize) {
27 | //获取结果
28 | const content = await zip.generateAsync({ type: "blob" });
29 | //保存文件
30 | const name = currentZipIndex ? `${zipName}(${currentZipIndex})` : zipName;
31 | saveAs(content, `${name}.zip`);
32 | //创建新的压缩包
33 | zip = new JSZip();
34 | currentZipSize = 0;
35 | currentZipIndex++;
36 | }
37 | //添加文件
38 | zip.file(file.name, file);
39 | currentZipSize += file.size;
40 | }
41 | // 生成并保存最后一个压缩包
42 | const content = await zip.generateAsync({ type: "blob" });
43 | if (Object.keys(zip.files).length > 0) {
44 | const name = currentZipIndex ? `${zipName}(${currentZipIndex})` : zipName;
45 | saveAs(content, `${name}.zip`);
46 | }
47 | };
48 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/tailwind.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./index.html",
5 | "./src/**/*.{js,ts,jsx,tsx}",
6 | "./node_modules/flowbite/**/*.js",
7 | ],
8 | theme: {
9 | extend: {},
10 | },
11 | plugins: [require("flowbite/plugin")],
12 | };
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "lib": [
6 | "DOM",
7 | "DOM.Iterable",
8 | "ESNext"
9 | ],
10 | "allowJs": false,
11 | "skipLibCheck": true,
12 | "esModuleInterop": false,
13 | "allowSyntheticDefaultImports": true,
14 | "strict": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "module": "ESNext",
17 | "moduleResolution": "Node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx",
22 | "baseUrl": ".",
23 | "paths": {
24 | "@/*": [
25 | "src/*"
26 | ]
27 | }
28 | },
29 | "include": [
30 | "src"
31 | ],
32 | "references": [
33 | {
34 | "path": "./tsconfig.node.json"
35 | }
36 | ]
37 | }
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": ["vite.config.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react";
3 | import svgr from "vite-plugin-svgr";
4 | import path from "path";
5 |
6 | // https://vitejs.dev/config/
7 | export default defineConfig({
8 | plugins: [react(), svgr()],
9 | resolve: {
10 | alias: {
11 | "@": path.resolve(__dirname, "./src"),
12 | },
13 | },
14 | base: "./",
15 | });
16 |
--------------------------------------------------------------------------------