Full Code of nanxiaobei/antd-img-crop for AI

main 9ed87c0c7b5f cached
16 files
35.7 KB
9.6k tokens
14 symbols
1 requests
Download .txt
Repository: nanxiaobei/antd-img-crop
Branch: main
Commit: 9ed87c0c7b5f
Files: 16
Total size: 35.7 KB

Directory structure:
gitextract_qm1z0c07/

├── .gitignore
├── .prettierignore
├── .prettierrc.js
├── LICENSE
├── README.md
├── README.zh-CN.md
├── eslint.config.mjs
├── package.json
├── postcss.config.js
├── rollup.config.ts
├── src/
│   ├── EasyCrop.tsx
│   ├── ImgCrop.css
│   ├── ImgCrop.tsx
│   ├── constants.ts
│   └── types.ts
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
node_modules
dist

example
index.html
vite.config.mts


================================================
FILE: .prettierignore
================================================
dist


================================================
FILE: .prettierrc.js
================================================
module.exports = {
  singleQuote: true,
  plugins: ['prettier-plugin-organize-imports', 'prettier-plugin-tailwindcss'],
};


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2018 nanxiaobei

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
<div align="center">

Link in bio to **widgets**,
your online **home screen**. ➫ [🔗 kee.so](https://kee.so/)

</div>

---

# antd-img-crop

An image cropper for Ant Design [Upload](https://ant.design/components/upload/)

[![npm](https://img.shields.io/npm/v/antd-img-crop.svg?style=flat-square)](https://www.npmjs.com/package/antd-img-crop)
[![npm](https://img.shields.io/npm/dt/antd-img-crop?style=flat-square)](https://www.npmtrends.com/antd-img-crop)
[![npm bundle size](https://img.shields.io/bundlephobia/minzip/antd-img-crop?style=flat-square)](https://bundlephobia.com/result?p=antd-img-crop)
[![GitHub](https://img.shields.io/github/license/nanxiaobei/antd-img-crop?style=flat-square)](https://github.com/nanxiaobei/antd-img-crop/blob/main/LICENSE)
[![npm type definitions](https://img.shields.io/npm/types/typescript?style=flat-square)](https://github.com/nanxiaobei/antd-img-crop/blob/main/src/types.ts)

English | [简体中文](./README.zh-CN.md)

## Demo

[![Edit antd-img-crop](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/p/sandbox/antd-img-crop-5x4j3r)

## Install

```sh
pnpm add antd-img-crop
# or
yarn add antd-img-crop
# or
npm i antd-img-crop
```

## Usage

```jsx harmony
import { Upload } from 'antd';
import ImgCrop from 'antd-img-crop';

const Demo = () => (
  <ImgCrop>
    <Upload>+ Add image</Upload>
  </ImgCrop>
);
```

## Props

| Prop           | Type       | Default        | Description                                                                      |
| -------------- | ---------- | -------------- | -------------------------------------------------------------------------------- |
| quality        | `number`   | `0.4`          | Cropped image quality, `0` to `1`                                                |
| fillColor      | `string`   | `'white'`      | Fill color for cropped image                                                     |
| zoomSlider     | `boolean`  | `true`         | Enable zoom                                                                      |
| rotationSlider | `boolean`  | `false`        | Enable rotation                                                                  |
| aspectSlider   | `boolean`  | `false`        | Enable aspect                                                                    |
| showReset      | `boolean`  | `false`        | Show reset button to reset zoom & rotation & aspect                              |
| resetText      | `string`   | `Reset`        | Reset button text                                                                |
| aspect         | `number`   | `1 / 1`        | Aspect of crop area , `width / height`                                           |
| minZoom        | `number`   | `1`            | Minimum zoom                                                                     |
| maxZoom        | `number`   | `3`            | Maximum zoom                                                                     |
| minAspect      | `number`   | `0.5`          | Minimum aspect                                                                   |
| maxAspect      | `number`   | `2`            | Maximum aspect                                                                   |
| cropShape      | `string`   | `'rect'`       | Shape of crop area, `'rect'` or `'round'`                                        |
| showGrid       | `boolean`  | `false`        | Show grid of crop area (third-lines)                                             |
| cropperProps   | `object`   | -              | [react-easy-crop] props (\* existing props cannot be overridden)                 |
| modalClassName | `string`   | `''`           | Modal classname                                                                  |
| modalTitle     | `string`   | `'Edit image'` | Modal title                                                                      |
| modalWidth     | `number`   | `string`       | Modal width                                                                      |
| modalOk        | `string`   |                | Ok button text                                                                   |
| modalCancel    | `string`   |                | Cancel button text                                                               |
| onModalOk      | `function` | -              | Callback of click ok button                                                      |
| onModalCancel  | `function` | -              | Callback of click cancel button (or modal mask & top right "x")                  |
| modalProps     | `object`   |                | [Ant Design Modal] props (\* existing props cannot be overridden)                |
| beforeCrop     | `function` | -              | Callback before crop modal, if return `false` or `reject()`, modal will not open |

## FAQ

### `ConfigProvider` not work?

Try to set `libraryDirectory`(`'es'` or `'lib'`) to `babel-plugin-import` config, see which one will work.

```js
module.exports = {
  plugins: [
    ['import', { libraryName: 'antd', libraryDirectory: 'es', style: true }],
  ],
};
```

### No style? (only `antd<=v4`)

If you use `antd<=v4` + `babel-plugin-import`, and no `Modal` or `Slider` were imported, please import these styles manually:

```js
import 'antd/es/modal/style';
import 'antd/es/slider/style';
```

## License

[MIT License](https://github.com/nanxiaobei/antd-img-crop/blob/main/LICENSE) (c) [nanxiaobei](https://lee.so/)

[react-easy-crop]: https://github.com/ValentinH/react-easy-crop#props
[Ant Design Modal]: https://ant.design/components/modal#api


================================================
FILE: README.zh-CN.md
================================================
<div align="center">

Link in bio to **widgets**,
your online **home screen**. ➫ [🔗 kee.so](https://kee.so/)

</div>

---

# antd-img-crop

图片裁切工具,用于 Ant Design [Upload](https://ant.design/components/upload-cn/) 组件

[![npm](https://img.shields.io/npm/v/antd-img-crop.svg?style=flat-square)](https://www.npmjs.com/package/antd-img-crop)
[![npm](https://img.shields.io/npm/dt/antd-img-crop?style=flat-square)](https://www.npmtrends.com/antd-img-crop)
[![npm bundle size](https://img.shields.io/bundlephobia/minzip/antd-img-crop?style=flat-square)](https://bundlephobia.com/result?p=antd-img-crop)
[![GitHub](https://img.shields.io/github/license/nanxiaobei/antd-img-crop?style=flat-square)](https://github.com/nanxiaobei/antd-img-crop/blob/main/LICENSE)
[![npm type definitions](https://img.shields.io/npm/types/typescript?style=flat-square)](https://github.com/nanxiaobei/antd-img-crop/blob/main/src/types.ts)

[English](./README.md) | 简体中文

## 示例

[![Edit antd-img-crop](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/p/sandbox/antd-img-crop-5x4j3r)

## 安装

```sh
pnpm add antd-img-crop
# or
yarn add antd-img-crop
# or
npm i antd-img-crop
```

## 使用

```jsx harmony
import { Upload } from 'antd';
import ImgCrop from 'antd-img-crop';

const Demo = () => (
  <ImgCrop>
    <Upload>+ Add image</Upload>
  </ImgCrop>
);
```

## Props

| 属性           | 类型                 | 默认         | 说明                                                             |
| -------------- | -------------------- | ------------ | ---------------------------------------------------------------- |
| quality        | `number`             | `0.4`        | 裁切图片质量,`0` 到 `1` 之间                                    |
| fillColor      | `string`             | `'white'`    | 裁切图像填充色                                                   |
| zoomSlider     | `boolean`            | `true`       | 允许缩放                                                         |
| rotationSlider | `boolean`            | `false`      | 允许旋转                                                         |
| aspectSlider   | `boolean`            | `false`      | 允许调整裁切比                                                   |
| showReset      | `boolean`            | `false`      | 显示重置按钮,重置缩放 & 旋转 & 裁切比                           |
| resetText      | `string`             | `重置`       | 重置按钮文字                                                     |
| aspect         | `number`             | `1 / 1`      | 裁切区域宽高比,`width / height`                                 |
| minZoom        | `number`             | `1`          | 最小缩放                                                         |
| maxZoom        | `number`             | `3`          | 最大缩放                                                         |
| minAspect      | `number`             | `0.5`        | 最小裁切比                                                       |
| maxAspect      | `number`             | `2`          | 最大裁切比                                                       |
| cropShape      | `string`             | `'rect'`     | 裁切区域形状,`'rect'` 或 `'round'`                              |
| showGrid       | `boolean`            | `false`      | 显示裁切区域网格(九宫格)                                       |
| cropperProps   | `object`             | -            | [react-easy-crop] 的 props(\* 已有 props 无法重写)             |
| modalClassName | `string`             | `''`         | 弹窗 className                                                   |
| modalTitle     | `string`             | `'编辑图片'` | 弹窗标题                                                         |
| modalWidth     | `number` \| `string` |              | 弹窗宽度                                                         |
| modalOk        | `string`             |              | 确定按钮文字                                                     |
| modalCancel    | `string`             |              | 取消按钮文字                                                     |
| onModalOK      | `function`           | -            | 点击确定按钮的回调                                               |
| onModalCancel  | `function`           | -            | 点击取消按钮、遮罩层、或右上角 'x' 的回调                        |
| modalProps     | `object`             |              | [Ant Design Modal] 的 props(\* 已有 props 无法重写)            |
| beforeCrop     | `function`           | -            | 裁切弹窗打开前的回调,若返回 `false` 或 `reject`,弹窗将不会打开 |

## FAQ

### `ConfigProvider` 无效?

尝试设置 `libraryDirectory`(`'es'` 或 `'lib'`)到 `babel-plugin-import` 的配置项,看看哪个会生效。

```js
module.exports = {
  plugins: [
    ['import', { libraryName: 'antd', libraryDirectory: 'es', style: true }],
  ],
};
```

### 没有样式?(仅 `antd<=v4`)

若使用 `antd<=v4` + `babel-plugin-import`,且未引入 `Modal` 或 `Slider`,请手动引入这些样式:

```js
import 'antd/es/modal/style';
import 'antd/es/slider/style';
```

## 协议

[MIT License](https://github.com/nanxiaobei/antd-img-crop/blob/main/LICENSE) (c) [nanxiaobei](https://lee.so/)

[react-easy-crop]: https://github.com/ValentinH/react-easy-crop#props
[Ant Design Modal]: https://ant.design/components/modal-cn#api


================================================
FILE: eslint.config.mjs
================================================
import { fixupConfigRules } from '@eslint/compat';
import { FlatCompat } from '@eslint/eslintrc';
import js from '@eslint/js';
import { defineConfig, globalIgnores } from 'eslint/config';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
  baseDirectory: __dirname,
  recommendedConfig: js.configs.recommended,
  allConfig: js.configs.all,
});

export default defineConfig([
  globalIgnores(['**/dist']),
  {
    extends: fixupConfigRules(compat.extends('react-app', 'prettier')),
  },
]);


================================================
FILE: package.json
================================================
{
  "name": "antd-img-crop",
  "version": "4.30.0",
  "description": "An image cropper for Ant Design Upload",
  "keywords": [
    "react",
    "antd",
    "ant-design",
    "upload",
    "image",
    "crop",
    "cropper"
  ],
  "license": "MIT",
  "author": "nanxiaobei <nanxiaobei@gmail.com> (https://github.com/nanxiaobei)",
  "homepage": "https://github.com/nanxiaobei/antd-img-crop",
  "repository": "github:nanxiaobei/antd-img-crop",
  "bugs": "https://github.com/nanxiaobei/antd-img-crop/issues",
  "types": "./dist/antd-img-crop.d.ts",
  "module": "./dist/antd-img-crop.esm.js",
  "main": "./dist/antd-img-crop.cjs.js",
  "exports": {
    ".": {
      "types": "./dist/antd-img-crop.d.ts",
      "import": "./dist/antd-img-crop.esm.js",
      "require": "./dist/antd-img-crop.cjs.js"
    }
  },
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "rm -rf dist && rollup -c --configPlugin @rollup/plugin-typescript --bundleConfigAsCjs"
  },
  "peerDependencies": {
    "antd": ">=4.0.0",
    "react": ">=16.8.0",
    "react-dom": ">=16.8.0"
  },
  "dependencies": {
    "react-easy-crop": "^5.5.6",
    "tslib": "^2.8.1"
  },
  "devDependencies": {
    "@eslint/compat": "^2.0.3",
    "@eslint/eslintrc": "^3.3.5",
    "@eslint/js": "^10.0.1",
    "@rollup/plugin-node-resolve": "^16.0.3",
    "@rollup/plugin-replace": "^6.0.3",
    "@rollup/plugin-typescript": "^12.3.0",
    "@tailwindcss/postcss": "^4.2.1",
    "@types/node": "^25.5.0",
    "@types/react": "19.2.14",
    "@types/react-dom": "19.2.3",
    "@typescript-eslint/eslint-plugin": "^8.57.0",
    "@vitejs/plugin-react": "^6.0.0",
    "antd": "^6.3.2",
    "autoprefixer": "^10.4.27",
    "eslint": "^10.0.3",
    "eslint-config-prettier": "^10.1.8",
    "eslint-config-react-app": "^7.0.1",
    "eslint-plugin-flowtype": "^8.0.3",
    "eslint-plugin-import": "^2.32.0",
    "eslint-plugin-jsx-a11y": "^6.10.2",
    "eslint-plugin-react": "^7.37.5",
    "eslint-plugin-react-hooks": "^7.0.1",
    "postcss": "^8.5.8",
    "prettier": "^3.8.1",
    "prettier-plugin-organize-imports": "^4.3.0",
    "prettier-plugin-tailwindcss": "^0.7.2",
    "react": "19.2.4",
    "react-dom": "19.2.4",
    "rollup": "4.59.0",
    "rollup-plugin-dts": "^6.4.0",
    "rollup-plugin-postcss": "^4.0.2",
    "tailwindcss": "^4.2.1",
    "typescript": "^5.9.3",
    "vite": "^8.0.0"
  }
}


================================================
FILE: postcss.config.js
================================================
module.exports = {
  plugins: {
    '@tailwindcss/postcss': {},
  },
};


================================================
FILE: rollup.config.ts
================================================
import nodeResolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import typescript from '@rollup/plugin-typescript';
import type { RollupOptions } from 'rollup';
import dts from 'rollup-plugin-dts';
import postcss from 'rollup-plugin-postcss';
import pkg from './package.json';

const input = 'src/ImgCrop.tsx';

const cjsOutput = { file: pkg.main, format: 'cjs', exports: 'auto' } as const;
const esmOutput = { file: pkg.module, format: 'es' } as const;
const dtsOutput = { file: pkg.types, format: 'es' } as const;

const nodePlugin = nodeResolve();
const tsPlugin = typescript({ compilerOptions: { module: 'esnext' } });
const postcssPlugin = postcss({
  minimize: true,
  inject: (cssVariableName) => `
    (function() {
      if (typeof document === 'undefined') return;
      const style = document.createElement('style');
      const meta = document.querySelector('meta[name="csp-nonce"]');
      if (meta && meta.content) style.setAttribute('nonce', meta.content);
      style.textContent = ${cssVariableName};
      document.head.appendChild(style);
    })();
    `,
});
const replacePlugin = replace({ preventAssignment: true, '/es/': '/lib/' });

const cjsPlugins = [nodePlugin, tsPlugin, postcssPlugin, replacePlugin];
const esmPlugins = [nodePlugin, tsPlugin, postcssPlugin];

const external = [
  ...Object.keys({ ...pkg.dependencies, ...pkg.peerDependencies }),
  /^react($|\/)/,
  /^antd($|\/)/,
];

const config: RollupOptions[] = [
  { input, output: cjsOutput, plugins: cjsPlugins, external },
  { input, output: esmOutput, plugins: esmPlugins, external },
  { input, output: dtsOutput, plugins: [dts()], external: [/\.css$/] },
];

export default config;


================================================
FILE: src/EasyCrop.tsx
================================================
import AntButton from 'antd/es/button';
import AntSlider from 'antd/es/slider';
import {
  forwardRef,
  memo,
  useCallback,
  useImperativeHandle,
  useRef,
  useState,
} from 'react';
import Cropper from 'react-easy-crop';
import type { Area, Point } from 'react-easy-crop';
import {
  ASPECT_STEP,
  PREFIX,
  ROTATION_INITIAL,
  ROTATION_MAX,
  ROTATION_MIN,
  ROTATION_STEP,
  ZOOM_INITIAL,
  ZOOM_STEP,
} from './constants';
import type { EasyCropProps, EasyCropRef } from './types';

const EasyCrop = forwardRef<EasyCropRef, EasyCropProps>((props, ref) => {
  const {
    cropperRef,
    zoomSlider,
    rotationSlider,
    aspectSlider,
    showReset,
    resetBtnText,

    modalImage,
    aspect: propAspect,
    minZoom,
    maxZoom,
    minAspect,
    maxAspect,
    cropShape,
    showGrid,

    cropperProps,
  } = props;

  const [crop, setCrop] = useState<Point>({ x: 0, y: 0 });
  const [zoom, setZoom] = useState(ZOOM_INITIAL);
  const [rotation, setRotation] = useState(ROTATION_INITIAL);
  const [aspect, setAspect] = useState(propAspect);

  const cropPixelsRef = useRef<Area>({ width: 0, height: 0, x: 0, y: 0 });
  const onCropComplete = useCallback((_: Area, croppedAreaPixels: Area) => {
    cropPixelsRef.current = croppedAreaPixels;
  }, []);

  const prevPropAspect = useRef(propAspect);
  if (prevPropAspect.current !== propAspect) {
    prevPropAspect.current = propAspect;
    setAspect(propAspect);
  }

  const isResetActive =
    zoom !== ZOOM_INITIAL ||
    rotation !== ROTATION_INITIAL ||
    aspect !== propAspect;

  const onReset = () => {
    setZoom(ZOOM_INITIAL);
    setRotation(ROTATION_INITIAL);
    setAspect(propAspect);
  };

  useImperativeHandle(ref, () => ({
    rotation,
    cropPixelsRef,
    onReset,
  }));

  const wrapperClass =
    '[display:flex] [align-items:center] [width:60%] [margin-inline:auto]';

  const buttonClass =
    '[display:flex] [align-items:center] [justify-content:center] [height:32px] [width:32px] [background:transparent] [border:0] [font-family:inherit] [font-size:18px] [cursor:pointer] disabled:[opacity:20%] disabled:[cursor:default]';

  const sliderClass = '[flex:1]';

  return (
    <>
      <Cropper
        {...cropperProps}
        ref={cropperRef}
        image={modalImage}
        crop={crop}
        zoom={zoom}
        rotation={rotation}
        aspect={aspect}
        minZoom={minZoom}
        maxZoom={maxZoom}
        zoomWithScroll={zoomSlider}
        cropShape={cropShape}
        showGrid={showGrid}
        onCropChange={setCrop}
        onZoomChange={setZoom}
        onRotationChange={setRotation}
        onCropComplete={onCropComplete}
        classes={{
          containerClassName: `${PREFIX}-container ![position:relative] [width:100%] [height:40vh] [&~section:first-of-type]:[margin-top:16px] [&~section:last-of-type]:[margin-bottom:16px]`,
          mediaClassName: `${PREFIX}-media`,
        }}
      />

      {zoomSlider && (
        <section
          className={`${PREFIX}-control ${PREFIX}-control-zoom ${wrapperClass}`}
        >
          <button
            className={buttonClass}
            onClick={() => setZoom(+(zoom - ZOOM_STEP).toFixed(1))}
            disabled={zoom - ZOOM_STEP < minZoom}
          >
            -
          </button>
          <AntSlider
            className={sliderClass}
            min={minZoom}
            max={maxZoom}
            step={ZOOM_STEP}
            value={zoom}
            onChange={setZoom}
          />
          <button
            className={buttonClass}
            onClick={() => setZoom(+(zoom + ZOOM_STEP).toFixed(1))}
            disabled={zoom + ZOOM_STEP > maxZoom}
          >
            +
          </button>
        </section>
      )}

      {rotationSlider && (
        <section
          className={`${PREFIX}-control ${PREFIX}-control-rotation ${wrapperClass}`}
        >
          <button
            className={`${buttonClass} [font-size:16px]`}
            onClick={() => setRotation(rotation - ROTATION_STEP)}
            disabled={rotation === ROTATION_MIN}
          >
            ↺
          </button>
          <AntSlider
            className={sliderClass}
            min={ROTATION_MIN}
            max={ROTATION_MAX}
            step={ROTATION_STEP}
            value={rotation}
            onChange={setRotation}
          />
          <button
            className={`${buttonClass} [font-size:16px]`}
            onClick={() => setRotation(rotation + ROTATION_STEP)}
            disabled={rotation === ROTATION_MAX}
          >
            ↻
          </button>
        </section>
      )}

      {aspectSlider && (
        <section
          className={`${PREFIX}-control ${PREFIX}-control-aspect ${wrapperClass}`}
        >
          <button
            className={buttonClass}
            onClick={() => setAspect(+(aspect - ASPECT_STEP).toFixed(2))}
            disabled={aspect - ASPECT_STEP < minAspect}
          >
            ↕
          </button>
          <AntSlider
            className={sliderClass}
            min={minAspect}
            max={maxAspect}
            step={ASPECT_STEP}
            value={aspect}
            onChange={setAspect}
          />
          <button
            className={buttonClass}
            onClick={() => setAspect(+(aspect + ASPECT_STEP).toFixed(2))}
            disabled={aspect + ASPECT_STEP > maxAspect}
          >
            ↔
          </button>
        </section>
      )}

      {showReset && (zoomSlider || rotationSlider || aspectSlider) && (
        <AntButton
          className="[position:absolute] [bottom:20px]"
          style={isResetActive ? {} : { opacity: 0.3, pointerEvents: 'none' }}
          onClick={onReset}
        >
          {resetBtnText}
        </AntButton>
      )}
    </>
  );
});

export default memo(EasyCrop);


================================================
FILE: src/ImgCrop.css
================================================
@tailwind utilities;


================================================
FILE: src/ImgCrop.tsx
================================================
import type { ModalProps } from 'antd';
import { version } from 'antd';
import AntModal from 'antd/es/modal';
import AntUpload from 'antd/es/upload';
import type { RcFile, UploadFile } from 'antd/es/upload/interface';
import type { MouseEvent, ReactNode } from 'react';
import { forwardRef, useCallback, useMemo, useRef, useState } from 'react';
import type CropperRef from 'react-easy-crop';
import EasyCrop from './EasyCrop';
import './ImgCrop.css';
import { PREFIX, ROTATION_INITIAL } from './constants';
import type {
  BeforeUpload,
  BeforeUploadReturnType,
  EasyCropRef,
  ImgCropProps,
} from './types';

// v1 >= v2
const isGeThan = (v1: string, v2: string): boolean => {
  const arr1 = v1.split('.').map(Number);
  const arr2 = v2.split('.').map(Number);
  const len = Math.max(arr1.length, arr2.length);
  for (let i = 0; i < len; i++) {
    const a = arr1[i] ?? 0;
    const b = arr2[i] ?? 0;
    if (a > b) return true;
    if (a < b) return false;
  }
  return true;
};

export type { ImgCropProps } from './types';

const ImgCrop = forwardRef<CropperRef, ImgCropProps>((props, cropperRef) => {
  const {
    quality = 0.4,
    fillColor = 'white',

    zoomSlider = true,
    rotationSlider = false,
    aspectSlider = false,
    showReset = false,
    resetText,

    aspect = 1,
    minZoom = 1,
    maxZoom = 3,
    minAspect = 0.5,
    maxAspect = 2,
    cropShape = 'rect',
    showGrid = false,
    cropperProps,

    modalClassName,
    modalTitle,
    modalWidth,
    modalOk,
    modalCancel,
    onModalOk,
    onModalCancel,
    modalProps,

    beforeCrop,
    children,
  } = props;

  const cb = useRef<
    Pick<ImgCropProps, 'onModalOk' | 'onModalCancel' | 'beforeCrop'>
  >({});
  cb.current.onModalOk = onModalOk;
  cb.current.onModalCancel = onModalCancel;
  cb.current.beforeCrop = beforeCrop;

  /**
   * crop
   */
  const easyCropRef = useRef<EasyCropRef>(null);
  const getCropCanvas = useCallback(
    (target: ShadowRoot) => {
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
      const context = (target?.getRootNode?.() as ShadowRoot) || document;

      type ImgSource = CanvasImageSource & {
        naturalWidth: number;
        naturalHeight: number;
      };

      const imgSource = context.querySelector(`.${PREFIX}-media`) as ImgSource;

      const {
        width: cropWidth,
        height: cropHeight,
        x: cropX,
        y: cropY,
      } = easyCropRef.current!.cropPixelsRef.current;

      if (
        rotationSlider &&
        easyCropRef.current!.rotation !== ROTATION_INITIAL
      ) {
        const { naturalWidth: imgWidth, naturalHeight: imgHeight } = imgSource;
        const angle = easyCropRef.current!.rotation * (Math.PI / 180);

        // get container for rotated image
        const sine = Math.abs(Math.sin(angle));
        const cosine = Math.abs(Math.cos(angle));
        const squareWidth = imgWidth * cosine + imgHeight * sine;
        const squareHeight = imgHeight * cosine + imgWidth * sine;

        canvas.width = squareWidth;
        canvas.height = squareHeight;
        ctx.fillStyle = fillColor;
        ctx.fillRect(0, 0, squareWidth, squareHeight);

        // rotate container
        const squareHalfWidth = squareWidth / 2;
        const squareHalfHeight = squareHeight / 2;
        ctx.translate(squareHalfWidth, squareHalfHeight);
        ctx.rotate(angle);
        ctx.translate(-squareHalfWidth, -squareHalfHeight);

        // draw rotated image
        const imgX = (squareWidth - imgWidth) / 2;
        const imgY = (squareHeight - imgHeight) / 2;
        ctx.drawImage(
          imgSource,
          0,
          0,
          imgWidth,
          imgHeight,
          imgX,
          imgY,
          imgWidth,
          imgHeight,
        );

        // crop rotated image
        const imgData = ctx.getImageData(0, 0, squareWidth, squareHeight);
        canvas.width = cropWidth;
        canvas.height = cropHeight;
        ctx.putImageData(imgData, -cropX, -cropY);
      } else {
        canvas.width = cropWidth;
        canvas.height = cropHeight;
        ctx.fillStyle = fillColor;
        ctx.fillRect(0, 0, cropWidth, cropHeight);

        ctx.drawImage(
          imgSource,
          cropX,
          cropY,
          cropWidth,
          cropHeight,
          0,
          0,
          cropWidth,
          cropHeight,
        );
      }

      return canvas;
    },
    [fillColor, rotationSlider],
  );

  /**
   * upload
   */
  const [modalOpen, setModalOpen] = useState(false);
  const [modalImage, setModalImage] = useState('');
  const onCancel = useRef<ModalProps['onCancel']>(undefined);
  const onOk = useRef<ModalProps['onOk']>(undefined);

  const runBeforeUpload = useCallback(
    async ({
      beforeUpload,
      file,
      resolve,
      reject,
    }: {
      beforeUpload: BeforeUpload;
      file: RcFile;
      resolve: (parsedFile: BeforeUploadReturnType) => void;
      reject: (rejectErr: BeforeUploadReturnType) => void;
    }) => {
      const rawFile = file as unknown as File;

      if (typeof beforeUpload !== 'function') {
        resolve(rawFile);
        return;
      }

      try {
        // https://ant.design/components/upload-cn#api
        // https://github.com/ant-design/ant-design/blob/master/components/upload/Upload.tsx#L152-L178
        const result = await beforeUpload(file, [file]);

        if (result === false) {
          resolve(false);
        } else {
          resolve((result !== true && result) || rawFile);
        }
      } catch (err) {
        reject(err as BeforeUploadReturnType);
      }
    },
    [],
  );

  const getNewBeforeUpload = useCallback(
    (beforeUpload: BeforeUpload) => {
      return ((file, fileList) => {
        return new Promise(async (resolve, reject) => {
          let processedFile = file;

          if (typeof cb.current.beforeCrop === 'function') {
            try {
              const result = await cb.current.beforeCrop(file, fileList);
              if (result === false) {
                return runBeforeUpload({ beforeUpload, file, resolve, reject }); // not open modal
              }
              if (result !== true) {
                processedFile = (result as unknown as RcFile) || file; // will open modal
              }
            } catch (err) {
              return runBeforeUpload({ beforeUpload, file, resolve, reject }); // not open modal
            }
          }

          // read file
          const reader = new FileReader();
          reader.addEventListener('load', () => {
            if (typeof reader.result === 'string') {
              setModalOpen(true);
              setTimeout(() => {
                setModalImage(reader.result as string);
              }, 10);
            }
          });
          reader.readAsDataURL(processedFile as unknown as Blob);

          // on modal cancel
          onCancel.current = () => {
            setModalOpen(false);
            setModalImage('');
            easyCropRef.current!.onReset();

            let hasResolveCalled = false;

            cb.current.onModalCancel?.((LIST_IGNORE) => {
              resolve(LIST_IGNORE);
              hasResolveCalled = true;
            });

            if (!hasResolveCalled) {
              resolve(AntUpload.LIST_IGNORE);
            }
          };

          // on modal confirm
          onOk.current = async (event: MouseEvent<HTMLElement>) => {
            setModalOpen(false);
            setModalImage('');
            easyCropRef.current!.onReset();

            const canvas = getCropCanvas(event.target as ShadowRoot);
            const { type, name, uid } = processedFile as UploadFile;

            canvas.toBlob(
              async (blob) => {
                const newFile = new File([blob as BlobPart], name, { type });
                Object.assign(newFile, { uid });

                runBeforeUpload({
                  beforeUpload,
                  file: newFile as unknown as RcFile,
                  resolve: (file) => {
                    resolve(file);
                    cb.current.onModalOk?.(file);
                  },
                  reject: (err) => {
                    reject(err);
                    cb.current.onModalOk?.(err);
                  },
                });
              },
              type,
              quality,
            );
          };
        });
      }) as BeforeUpload;
    },
    [getCropCanvas, quality, runBeforeUpload],
  );

  const getNewUpload = useCallback(
    (children: ReactNode) => {
      const upload = Array.isArray(children) ? children[0] : children;
      const { beforeUpload, accept, ...restUploadProps } = upload.props;

      return {
        ...upload,
        props: {
          ...restUploadProps,
          accept: accept || 'image/*',
          beforeUpload: getNewBeforeUpload(beforeUpload),
        },
      };
    },
    [getNewBeforeUpload],
  );

  /**
   * modal
   */
  const modalBaseProps = useMemo(() => {
    const obj: Pick<ModalProps, 'width' | 'okText' | 'cancelText'> = {};
    if (modalWidth !== undefined) obj.width = modalWidth;
    if (modalOk !== undefined) obj.okText = modalOk;
    if (modalCancel !== undefined) obj.cancelText = modalCancel;
    return obj;
  }, [modalCancel, modalOk, modalWidth]);

  const wrapClassName = `${PREFIX}-modal${
    modalClassName ? ` ${modalClassName}` : ''
  }`;

  const lang = typeof window === 'undefined' ? '' : window.navigator.language;
  const isCN = lang === 'zh-CN';
  const title = modalTitle || (isCN ? '编辑图片' : 'Edit image');
  const resetBtnText = resetText || (isCN ? '重置' : 'Reset');

  return (
    <>
      {getNewUpload(children)}
      <AntModal
        {...modalProps}
        {...modalBaseProps}
        open={modalOpen}
        title={title}
        onCancel={onCancel.current}
        onOk={onOk.current}
        wrapClassName={wrapClassName}
        destroyOnHidden
        {...(isGeThan(version, '6.3.1')
          ? { mask: { closable: false } }
          : { maskClosable: true })}
      >
        <EasyCrop
          ref={easyCropRef}
          cropperRef={cropperRef}
          zoomSlider={zoomSlider}
          rotationSlider={rotationSlider}
          aspectSlider={aspectSlider}
          showReset={showReset}
          resetBtnText={resetBtnText}
          modalImage={modalImage}
          aspect={aspect}
          minZoom={minZoom}
          maxZoom={maxZoom}
          minAspect={minAspect}
          maxAspect={maxAspect}
          cropShape={cropShape}
          showGrid={showGrid}
          cropperProps={cropperProps}
        />
      </AntModal>
    </>
  );
});

export default ImgCrop;


================================================
FILE: src/constants.ts
================================================
export const PREFIX = 'img-crop';

export const ZOOM_INITIAL = 1;
export const ZOOM_STEP = 0.1;

export const ROTATION_INITIAL = 0;
export const ROTATION_MIN = -180;
export const ROTATION_MAX = 180;
export const ROTATION_STEP = 1;

export const ASPECT_STEP = 0.01;


================================================
FILE: src/types.ts
================================================
import type { ModalProps, UploadProps } from 'antd';
import { ForwardedRef, JSX, MutableRefObject } from 'react';
import type { default as Cropper, CropperProps } from 'react-easy-crop';
import type { Area } from 'react-easy-crop';

export type BeforeUpload = Exclude<UploadProps['beforeUpload'], undefined>;
export type BeforeUploadReturnType = ReturnType<BeforeUpload>;

export type ImgCropProps = {
  quality?: number;
  fillColor?: string;

  zoomSlider?: boolean;
  rotationSlider?: boolean;
  aspectSlider?: boolean;
  showReset?: boolean;
  resetText?: string;

  aspect?: number;
  minZoom?: number;
  maxZoom?: number;
  minAspect?: number;
  maxAspect?: number;
  cropShape?: 'rect' | 'round';
  showGrid?: boolean;
  cropperProps?: Omit<
    CropperProps,
    | 'image'
    | 'crop'
    | 'zoom'
    | 'rotation'
    | 'aspect'
    | 'minZoom'
    | 'maxZoom'
    | 'minAspect'
    | 'maxAspect'
    | 'zoomWithScroll'
    | 'cropShape'
    | 'showGrid'
    | 'onCropChange'
    | 'onZoomChange'
    | 'onRotationChange'
    | 'onCropComplete'
    | 'classes'
    | 'keyboardStep'
  > &
    Partial<Pick<CropperProps, 'keyboardStep'>>;

  modalClassName?: string;
  modalTitle?: string;
  modalWidth?: number | string;
  modalOk?: string;
  modalCancel?: string;
  onModalOk?: (value: BeforeUploadReturnType) => void;
  onModalCancel?: (resolve: (value: BeforeUploadReturnType) => void) => void;
  modalProps?: Omit<
    ModalProps,
    | 'className'
    | 'title'
    | 'width'
    | 'okText'
    | 'cancelText'
    | 'onOk'
    | 'onCancel'
    | 'open'
    | 'visible'
    | 'wrapClassName'
    | 'maskClosable'
    | 'destroyOnHidden'
  >;

  beforeCrop?: BeforeUpload;
  children: JSX.Element;
};

export type EasyCropRef = {
  rotation: number;
  cropPixelsRef: MutableRefObject<Area>;
  onReset: () => void;
};

export type EasyCropProps = {
  cropperRef: ForwardedRef<Cropper>;
  modalImage: string;
} & Required<
  Pick<
    ImgCropProps,
    | 'zoomSlider'
    | 'rotationSlider'
    | 'aspectSlider'
    | 'showReset'
    | 'aspect'
    | 'minZoom'
    | 'maxZoom'
    | 'minAspect'
    | 'maxAspect'
    | 'cropShape'
    | 'showGrid'
  >
> &
  Pick<ImgCropProps, 'cropperProps'> & { resetBtnText: string };


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "alwaysStrict": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitAny": true,
    "noImplicitOverride": true,
    "noImplicitReturns": false,
    "noImplicitThis": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "strict": true,
    "strictBindCallApply": true,
    "strictFunctionTypes": true,
    "strictNullChecks": true,
    "skipLibCheck": true,
    "strictPropertyInitialization": true,

    "module": "nodenext",
    "moduleResolution": "nodenext",
    "resolveJsonModule": true,

    "outDir": "dist",

    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,

    "jsx": "react-jsx",
    "jsxImportSource": "react",
    "lib": ["esnext", "dom", "dom.iterable"],
    "target": "es6"
  },
  "exclude": ["node_modules", "dist"]
}
Download .txt
gitextract_qm1z0c07/

├── .gitignore
├── .prettierignore
├── .prettierrc.js
├── LICENSE
├── README.md
├── README.zh-CN.md
├── eslint.config.mjs
├── package.json
├── postcss.config.js
├── rollup.config.ts
├── src/
│   ├── EasyCrop.tsx
│   ├── ImgCrop.css
│   ├── ImgCrop.tsx
│   ├── constants.ts
│   └── types.ts
└── tsconfig.json
Download .txt
SYMBOL INDEX (14 symbols across 3 files)

FILE: src/ImgCrop.tsx
  type ImgSource (line 85) | type ImgSource = CanvasImageSource & {

FILE: src/constants.ts
  constant PREFIX (line 1) | const PREFIX = 'img-crop';
  constant ZOOM_INITIAL (line 3) | const ZOOM_INITIAL = 1;
  constant ZOOM_STEP (line 4) | const ZOOM_STEP = 0.1;
  constant ROTATION_INITIAL (line 6) | const ROTATION_INITIAL = 0;
  constant ROTATION_MIN (line 7) | const ROTATION_MIN = -180;
  constant ROTATION_MAX (line 8) | const ROTATION_MAX = 180;
  constant ROTATION_STEP (line 9) | const ROTATION_STEP = 1;
  constant ASPECT_STEP (line 11) | const ASPECT_STEP = 0.01;

FILE: src/types.ts
  type BeforeUpload (line 6) | type BeforeUpload = Exclude<UploadProps['beforeUpload'], undefined>;
  type BeforeUploadReturnType (line 7) | type BeforeUploadReturnType = ReturnType<BeforeUpload>;
  type ImgCropProps (line 9) | type ImgCropProps = {
  type EasyCropRef (line 76) | type EasyCropRef = {
  type EasyCropProps (line 82) | type EasyCropProps = {
Condensed preview — 16 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (39K chars).
[
  {
    "path": ".gitignore",
    "chars": 54,
    "preview": "node_modules\ndist\n\nexample\nindex.html\nvite.config.mts\n"
  },
  {
    "path": ".prettierignore",
    "chars": 5,
    "preview": "dist\n"
  },
  {
    "path": ".prettierrc.js",
    "chars": 123,
    "preview": "module.exports = {\n  singleQuote: true,\n  plugins: ['prettier-plugin-organize-imports', 'prettier-plugin-tailwindcss'],\n"
  },
  {
    "path": "LICENSE",
    "chars": 1067,
    "preview": "MIT License\n\nCopyright (c) 2018 nanxiaobei\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
  },
  {
    "path": "README.md",
    "chars": 5583,
    "preview": "<div align=\"center\">\n\nLink in bio to **widgets**,\nyour online **home screen**. ➫ [🔗 kee.so](https://kee.so/)\n\n</div>\n\n--"
  },
  {
    "path": "README.zh-CN.md",
    "chars": 5053,
    "preview": "<div align=\"center\">\n\nLink in bio to **widgets**,\nyour online **home screen**. ➫ [🔗 kee.so](https://kee.so/)\n\n</div>\n\n--"
  },
  {
    "path": "eslint.config.mjs",
    "chars": 640,
    "preview": "import { fixupConfigRules } from '@eslint/compat';\nimport { FlatCompat } from '@eslint/eslintrc';\nimport js from '@eslin"
  },
  {
    "path": "package.json",
    "chars": 2349,
    "preview": "{\n  \"name\": \"antd-img-crop\",\n  \"version\": \"4.30.0\",\n  \"description\": \"An image cropper for Ant Design Upload\",\n  \"keywor"
  },
  {
    "path": "postcss.config.js",
    "chars": 72,
    "preview": "module.exports = {\n  plugins: {\n    '@tailwindcss/postcss': {},\n  },\n};\n"
  },
  {
    "path": "rollup.config.ts",
    "chars": 1711,
    "preview": "import nodeResolve from '@rollup/plugin-node-resolve';\nimport replace from '@rollup/plugin-replace';\nimport typescript f"
  },
  {
    "path": "src/EasyCrop.tsx",
    "chars": 5809,
    "preview": "import AntButton from 'antd/es/button';\nimport AntSlider from 'antd/es/slider';\nimport {\n  forwardRef,\n  memo,\n  useCall"
  },
  {
    "path": "src/ImgCrop.css",
    "chars": 21,
    "preview": "@tailwind utilities;\n"
  },
  {
    "path": "src/ImgCrop.tsx",
    "chars": 10705,
    "preview": "import type { ModalProps } from 'antd';\nimport { version } from 'antd';\nimport AntModal from 'antd/es/modal';\nimport Ant"
  },
  {
    "path": "src/constants.ts",
    "chars": 265,
    "preview": "export const PREFIX = 'img-crop';\n\nexport const ZOOM_INITIAL = 1;\nexport const ZOOM_STEP = 0.1;\n\nexport const ROTATION_I"
  },
  {
    "path": "src/types.ts",
    "chars": 2231,
    "preview": "import type { ModalProps, UploadProps } from 'antd';\nimport { ForwardedRef, JSX, MutableRefObject } from 'react';\nimport"
  },
  {
    "path": "tsconfig.json",
    "chars": 886,
    "preview": "{\n  \"compilerOptions\": {\n    \"alwaysStrict\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noImplicitAny\": true,\n  "
  }
]

About this extraction

This page contains the full source code of the nanxiaobei/antd-img-crop GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 16 files (35.7 KB), approximately 9.6k tokens, and a symbol index with 14 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!