pixedi 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +65 -60
  2. package/README.npm.md +80 -0
  3. package/dist/lib/index.js +1602 -0
  4. package/dist/lib/index.umd.js +28 -0
  5. package/package.json +9 -4
  6. package/.github/workflows/deploy.yml +0 -54
  7. package/.prettierr +0 -9
  8. package/.storybook/main.ts +0 -16
  9. package/.storybook/preview.ts +0 -53
  10. package/.vscode/extensions.json +0 -6
  11. package/eslint.config.js +0 -22
  12. package/index.html +0 -13
  13. package/pnpm-workspace.yaml +0 -2
  14. package/public/favicon.svg +0 -1
  15. package/src/app/App.tsx +0 -10
  16. package/src/app/entries/index.ts +0 -145
  17. package/src/app/providers/RouterProvider.tsx +0 -6
  18. package/src/app/providers/index.ts +0 -1
  19. package/src/app/router/AppRouter.tsx +0 -12
  20. package/src/app/router/index.ts +0 -0
  21. package/src/app/styles/index.css +0 -4
  22. package/src/features/gallery/GalleryPage.tsx +0 -33
  23. package/src/features/gallery/ImageEditPage.tsx +0 -37
  24. package/src/features/gallery/index.ts +0 -2
  25. package/src/main.tsx +0 -10
  26. package/src/shared/components/ImageCard/ImageCard.tsx +0 -54
  27. package/src/shared/components/ImageCard/index.ts +0 -1
  28. package/src/shared/components/ImageEditor/ImageEditor.tsx +0 -48
  29. package/src/shared/components/ImageEditor/assets/icons.tsx +0 -114
  30. package/src/shared/components/ImageEditor/constants.ts +0 -118
  31. package/src/shared/components/ImageEditor/crop/Crop.tsx +0 -60
  32. package/src/shared/components/ImageEditor/crop/CropBorders.tsx +0 -28
  33. package/src/shared/components/ImageEditor/crop/CropLines.tsx +0 -8
  34. package/src/shared/components/ImageEditor/crop/CropPointer.tsx +0 -21
  35. package/src/shared/components/ImageEditor/crop/CropPointers.tsx +0 -44
  36. package/src/shared/components/ImageEditor/crop/CropTools.tsx +0 -138
  37. package/src/shared/components/ImageEditor/crop/crop.css +0 -180
  38. package/src/shared/components/ImageEditor/crop/index.ts +0 -8
  39. package/src/shared/components/ImageEditor/crop/useCropInteraction.ts +0 -230
  40. package/src/shared/components/ImageEditor/eventBus.ts +0 -8
  41. package/src/shared/components/ImageEditor/frame/Frame.tsx +0 -48
  42. package/src/shared/components/ImageEditor/frame/frame.css +0 -40
  43. package/src/shared/components/ImageEditor/frame/index.ts +0 -1
  44. package/src/shared/components/ImageEditor/header/Header.tsx +0 -81
  45. package/src/shared/components/ImageEditor/header/header.css +0 -27
  46. package/src/shared/components/ImageEditor/header/index.ts +0 -1
  47. package/src/shared/components/ImageEditor/hooks/index.ts +0 -5
  48. package/src/shared/components/ImageEditor/hooks/useAbove.tsx +0 -18
  49. package/src/shared/components/ImageEditor/hooks/useBellow.tsx +0 -18
  50. package/src/shared/components/ImageEditor/hooks/useImageLoader.tsx +0 -110
  51. package/src/shared/components/ImageEditor/hooks/useImageProcessor.test.tsx +0 -147
  52. package/src/shared/components/ImageEditor/hooks/useImageProcessor.ts +0 -225
  53. package/src/shared/components/ImageEditor/hooks/useMobile.tsx +0 -33
  54. package/src/shared/components/ImageEditor/index.css +0 -145
  55. package/src/shared/components/ImageEditor/index.ts +0 -1
  56. package/src/shared/components/ImageEditor/provider/ImageEditorProvider.test.tsx +0 -126
  57. package/src/shared/components/ImageEditor/provider/ImageEditorProvider.tsx +0 -107
  58. package/src/shared/components/ImageEditor/provider/StoreContext.tsx +0 -20
  59. package/src/shared/components/ImageEditor/provider/initialState.ts +0 -30
  60. package/src/shared/components/ImageEditor/provider/useImageEditorContext.tsx +0 -12
  61. package/src/shared/components/ImageEditor/resize/ResizeTools.tsx +0 -183
  62. package/src/shared/components/ImageEditor/resize/index.ts +0 -1
  63. package/src/shared/components/ImageEditor/resize/resize.css +0 -56
  64. package/src/shared/components/ImageEditor/sidebar/Sidebar.tsx +0 -54
  65. package/src/shared/components/ImageEditor/sidebar/SidebarCrop.tsx +0 -57
  66. package/src/shared/components/ImageEditor/sidebar/SidebarPresets.tsx +0 -54
  67. package/src/shared/components/ImageEditor/sidebar/SidebarResize.tsx +0 -26
  68. package/src/shared/components/ImageEditor/sidebar/index.ts +0 -1
  69. package/src/shared/components/ImageEditor/sidebar/sidebar.css +0 -101
  70. package/src/shared/components/ImageEditor/types.ts +0 -59
  71. package/src/shared/components/ImageEditor/ui/button/Button.stories.tsx +0 -42
  72. package/src/shared/components/ImageEditor/ui/button/Button.tsx +0 -21
  73. package/src/shared/components/ImageEditor/ui/button/button.css +0 -69
  74. package/src/shared/components/ImageEditor/ui/button-crop/ButtonCrop.stories.tsx +0 -26
  75. package/src/shared/components/ImageEditor/ui/button-crop/ButtonCrop.tsx +0 -29
  76. package/src/shared/components/ImageEditor/ui/button-crop/button-crop.css +0 -38
  77. package/src/shared/components/ImageEditor/ui/index.ts +0 -6
  78. package/src/shared/components/ImageEditor/ui/input/Input.stories.tsx +0 -45
  79. package/src/shared/components/ImageEditor/ui/input/Input.tsx +0 -20
  80. package/src/shared/components/ImageEditor/ui/input/input.css +0 -48
  81. package/src/shared/components/ImageEditor/ui/input-pixel/InputPixel.stories.tsx +0 -33
  82. package/src/shared/components/ImageEditor/ui/input-pixel/InputPixel.tsx +0 -23
  83. package/src/shared/components/ImageEditor/ui/input-pixel/input-pixel.css +0 -24
  84. package/src/shared/components/ImageEditor/ui/select/Select.stories.tsx +0 -67
  85. package/src/shared/components/ImageEditor/ui/select/Select.tsx +0 -161
  86. package/src/shared/components/ImageEditor/ui/select/select.css +0 -147
  87. package/src/shared/components/ImageEditor/ui/separator/Separator.stories.tsx +0 -37
  88. package/src/shared/components/ImageEditor/ui/separator/Separator.tsx +0 -27
  89. package/src/shared/components/ImageEditor/ui/separator/separator.css +0 -19
  90. package/src/shared/components/ImageEditor/utils.test.ts +0 -136
  91. package/src/shared/components/ImageEditor/utils.ts +0 -497
  92. package/src/shared/components/ui/index.ts +0 -1
  93. package/src/shared/components/ui/preloader.tsx +0 -78
  94. package/src/shared/config/index.ts +0 -3
  95. package/src/shared/config/routes.ts +0 -4
  96. package/src/shared/features/gallery/GalleryPage.tsx +0 -33
  97. package/src/shared/features/gallery/ImageEditPage.tsx +0 -37
  98. package/src/shared/features/gallery/index.ts +0 -2
  99. package/src/shared/hooks/index.ts +0 -1
  100. package/src/shared/hooks/useImagePreload.tsx +0 -49
  101. package/src/shared/types/index.ts +0 -11
  102. package/src/test/setup.ts +0 -14
  103. package/src/virtual-css-injected-by-js.d.ts +0 -8
  104. package/src/widget.tsx +0 -119
  105. package/tsconfig.app.json +0 -29
  106. package/tsconfig.json +0 -12
  107. package/tsconfig.lib.json +0 -18
  108. package/tsconfig.node.json +0 -23
  109. package/vite-env.d.ts +0 -1
  110. package/vite.config.ts +0 -24
  111. package/vite.lib.config.ts +0 -33
  112. package/vite.widget.config.ts +0 -39
  113. package/wrangler.json +0 -5
@@ -1,81 +0,0 @@
1
- import { useState } from "react";
2
- import { ArrowLeft, Check, Undo, Redo, Loader } from "../assets/icons";
3
- import { useImageEditorContext } from "../provider/useImageEditorContext";
4
- import { Button } from "../ui";
5
- import type { FuncSaveArgs } from "../types";
6
- import "./header.css";
7
-
8
- type HeaderProps = {
9
- onBack: () => void;
10
- onSave: FuncSaveArgs;
11
- };
12
-
13
- export const Header = ({ onBack, onSave }: HeaderProps) => {
14
- const [isSaving, setIsSaving] = useState(false);
15
- const {
16
- history,
17
- undo,
18
- redo,
19
- resetHistory,
20
- resetHistoryAfterSave,
21
- getLastHistoryItem,
22
- } = useImageEditorContext();
23
-
24
- const { base64 } = getLastHistoryItem();
25
- const showHistory = history.items.length > 1 && !isSaving;
26
- const disabledUndo = history.pointer === 0;
27
- const disabledRedo = history.pointer === history.items.length - 1;
28
- const disableSave = history.items.length < 2 || isSaving;
29
-
30
- const handleSave = async () => {
31
- if (!base64) return;
32
- setIsSaving(true);
33
- try {
34
- await onSave(base64);
35
- resetHistoryAfterSave();
36
- } finally {
37
- setIsSaving(false);
38
- }
39
- };
40
-
41
- return (
42
- <div className="header">
43
- <Button variant="ghost" className="btn-rect" onClick={onBack}>
44
- <ArrowLeft />
45
- </Button>
46
-
47
- <div className="header-tools">
48
- {showHistory && (
49
- <div className="header-history">
50
- <Button
51
- variant="outline"
52
- disabled={disabledUndo}
53
- className="btn-rect"
54
- onClick={undo}
55
- >
56
- <Undo />
57
- </Button>
58
- <div className="header-history-text">
59
- {history.pointer + 1}/{history.items.length}
60
- </div>
61
- <Button
62
- variant="outline"
63
- disabled={disabledRedo}
64
- className="btn-rect"
65
- onClick={redo}
66
- >
67
- <Redo />
68
- </Button>
69
- </div>
70
- )}
71
- <Button variant="outline" disabled={isSaving} onClick={resetHistory}>
72
- Reset
73
- </Button>
74
- <Button disabled={disableSave} onClick={handleSave}>
75
- {isSaving ? <Loader /> : <Check />}
76
- Save
77
- </Button>
78
- </div>
79
- </div>
80
- );
81
- };
@@ -1,27 +0,0 @@
1
- .header {
2
- height: 48px;
3
- display: flex;
4
- justify-content: space-between;
5
- align-items: center;
6
- padding-left: 8px;
7
- padding-right: 8px;
8
- box-sizing: border-box;
9
- background-color: var(--background);
10
- border-bottom: 1px solid var(--border);
11
- }
12
-
13
- .header-tools {
14
- display: flex;
15
- gap: 8px;
16
- }
17
-
18
- .header-history {
19
- display: flex;
20
- gap: 8px;
21
- align-items: center;
22
- }
23
-
24
- .header-history-text {
25
- color: var(--muted-foreground);
26
- font-size: 14px;
27
- }
@@ -1 +0,0 @@
1
- export { Header } from "./Header";
@@ -1,5 +0,0 @@
1
- export { useImageLoader } from "./useImageLoader";
2
- export { useImageProcessor } from "./useImageProcessor";
3
- export { useMobile } from "./useMobile";
4
- export { useBellow } from "./useBellow";
5
- export { useAbove } from "./useAbove";
@@ -1,18 +0,0 @@
1
- import { useEffect, useState } from "react";
2
- import { breakpoints, type Breakpoint } from "../types";
3
-
4
- export function useAbove(breakpoint: Breakpoint): boolean {
5
- const [isAbove, setIsAbove] = useState(
6
- () => window.innerWidth >= breakpoints[breakpoint],
7
- );
8
-
9
- useEffect(() => {
10
- function handleResize() {
11
- setIsAbove(window.innerWidth >= breakpoints[breakpoint]);
12
- }
13
- window.addEventListener("resize", handleResize);
14
- return () => window.removeEventListener("resize", handleResize);
15
- }, [breakpoint]);
16
-
17
- return isAbove;
18
- }
@@ -1,18 +0,0 @@
1
- import { useEffect, useState } from "react";
2
- import { breakpoints, type Breakpoint } from "../types";
3
-
4
- export function useBellow(breakpoint: Breakpoint): boolean {
5
- const [isBellow, setIsBellow] = useState(
6
- () => window.innerWidth < breakpoints[breakpoint],
7
- );
8
-
9
- useEffect(() => {
10
- function handleResize() {
11
- setIsBellow(window.innerWidth < breakpoints[breakpoint]);
12
- }
13
- window.addEventListener("resize", handleResize);
14
- return () => window.removeEventListener("resize", handleResize);
15
- }, [breakpoint]);
16
-
17
- return isBellow;
18
- }
@@ -1,110 +0,0 @@
1
- import { useEffect, useState } from "react";
2
-
3
- const mimeToExt: Record<string, string> = {
4
- "image/jpeg": "jpg",
5
- "image/jpg": "jpg",
6
- "image/png": "png",
7
- "image/gif": "gif",
8
- "image/webp": "webp",
9
- };
10
-
11
- type UseImageResult = {
12
- loading: boolean;
13
- error: string | null;
14
- width: number;
15
- height: number;
16
- extension: string | null;
17
- base64: string | null;
18
- };
19
-
20
- export const useImageLoader = (src: string): UseImageResult => {
21
- const [state, setState] = useState<UseImageResult>({
22
- loading: true,
23
- error: null,
24
- width: 0,
25
- height: 0,
26
- extension: null,
27
- base64: null,
28
- });
29
-
30
- useEffect(() => {
31
- let isMounted = true;
32
-
33
- const processImage = async () => {
34
- try {
35
- const response = await fetch(src);
36
- if (!response.ok) {
37
- throw new Error(`HTTP error! Status: ${response.status}`);
38
- }
39
-
40
- const mimeType = response.headers.get("content-type") || "";
41
- const cleanMime = mimeType.split(";")[0].toLowerCase().trim();
42
- const isImg = cleanMime.startsWith("image/");
43
-
44
- const extension = mimeToExt[cleanMime] || "unknown";
45
-
46
- if (!isImg) {
47
- if (isMounted) {
48
- setState((prev) => ({
49
- ...prev,
50
- loading: false,
51
- error: "The requested resource is not a valid image.",
52
- extension,
53
- }));
54
- }
55
- return;
56
- }
57
-
58
- const blob = await response.blob();
59
- const base64String = await new Promise<string>((resolve, reject) => {
60
- const reader = new FileReader();
61
- reader.onloadend = () => resolve(reader.result as string);
62
- reader.onerror = () =>
63
- reject(new Error("Failed to read image data stream"));
64
- reader.readAsDataURL(blob);
65
- });
66
-
67
- const dimensions = await new Promise<{ width: number; height: number }>(
68
- (resolve, reject) => {
69
- const img = new Image();
70
- img.src = base64String;
71
- img.onload = () =>
72
- resolve({ width: img.naturalWidth, height: img.naturalHeight });
73
- img.onerror = () =>
74
- reject(new Error("Failed to parse image dimensions"));
75
- },
76
- );
77
-
78
- if (isMounted) {
79
- setState({
80
- loading: false,
81
- error: null,
82
- width: dimensions.width,
83
- height: dimensions.height,
84
- extension,
85
- base64: base64String,
86
- });
87
- }
88
- } catch (err: unknown) {
89
- if (isMounted) {
90
- setState((prev) => ({
91
- ...prev,
92
- loading: false,
93
- error:
94
- err instanceof Error
95
- ? err.message
96
- : "An error occurred during extraction.",
97
- }));
98
- }
99
- }
100
- };
101
-
102
- processImage();
103
-
104
- return () => {
105
- isMounted = false;
106
- };
107
- }, [src]);
108
-
109
- return state;
110
- };
@@ -1,147 +0,0 @@
1
- import { renderHook } from "@testing-library/react";
2
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
- import { useImageProcessor } from "./useImageProcessor";
4
-
5
- const bitmap = {
6
- width: 120,
7
- height: 80,
8
- close: vi.fn(),
9
- };
10
-
11
- const context = {
12
- clearRect: vi.fn(),
13
- drawImage: vi.fn(),
14
- filter: "",
15
- restore: vi.fn(),
16
- rotate: vi.fn(),
17
- save: vi.fn(),
18
- scale: vi.fn(),
19
- translate: vi.fn(),
20
- };
21
-
22
- const validPng = "data:image/png;base64,aGVsbG8=";
23
- const validJpeg = "data:image/jpeg;base64,aGVsbG8=";
24
-
25
- describe("useImageProcessor", () => {
26
- beforeEach(() => {
27
- vi.clearAllMocks();
28
- vi.stubGlobal("createImageBitmap", vi.fn().mockResolvedValue(bitmap));
29
- vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue(
30
- context as unknown as CanvasRenderingContext2D,
31
- );
32
- vi.spyOn(HTMLCanvasElement.prototype, "toDataURL").mockReturnValue(
33
- "data:image/png;base64,processed",
34
- );
35
- });
36
-
37
- afterEach(() => {
38
- vi.restoreAllMocks();
39
- vi.unstubAllGlobals();
40
- });
41
-
42
- it("rejects malformed Base64 before attempting to decode an image", async () => {
43
- const { result } = renderHook(() => useImageProcessor());
44
-
45
- await expect(result.current.resize("invalid", 10, 10)).rejects.toThrow(
46
- "Invalid Base64 image format",
47
- );
48
- expect(createImageBitmap).not.toHaveBeenCalled();
49
- });
50
-
51
- it("resizes an image and serializes PNG output without a quality argument", async () => {
52
- const { result } = renderHook(() => useImageProcessor());
53
-
54
- await expect(result.current.resize(validPng, 60, 40)).resolves.toBe(
55
- "data:image/png;base64,processed",
56
- );
57
-
58
- expect(createImageBitmap).toHaveBeenCalledTimes(1);
59
- expect(context.clearRect).toHaveBeenCalledWith(0, 0, 60, 40);
60
- expect(context.drawImage).toHaveBeenCalledWith(bitmap, 0, 0, 60, 40);
61
- expect(bitmap.close).toHaveBeenCalledTimes(1);
62
- expect(HTMLCanvasElement.prototype.toDataURL).toHaveBeenCalledWith(
63
- "image/png",
64
- );
65
- });
66
-
67
- it("crops an image with source and destination rectangles", async () => {
68
- const { result } = renderHook(() => useImageProcessor());
69
-
70
- await result.current.crop(validPng, 5, 6, 50, 40);
71
-
72
- expect(context.drawImage).toHaveBeenCalledWith(
73
- bitmap,
74
- 5,
75
- 6,
76
- 50,
77
- 40,
78
- 0,
79
- 0,
80
- 50,
81
- 40,
82
- );
83
- expect(bitmap.close).toHaveBeenCalledTimes(1);
84
- });
85
-
86
- it("flips an image using the expected canvas transforms", async () => {
87
- const { result } = renderHook(() => useImageProcessor());
88
-
89
- await result.current.flip(validPng, true, false);
90
-
91
- expect(context.translate).toHaveBeenCalledWith(120, 0);
92
- expect(context.scale).toHaveBeenCalledWith(-1, 1);
93
- expect(context.drawImage).toHaveBeenCalledWith(bitmap, 0, 0);
94
- expect(context.restore).toHaveBeenCalledTimes(1);
95
- expect(bitmap.close).toHaveBeenCalledTimes(1);
96
- });
97
-
98
- it("rotates an image around its center", async () => {
99
- const { result } = renderHook(() => useImageProcessor());
100
-
101
- await result.current.rotate(validPng, 90);
102
-
103
- expect(context.translate).toHaveBeenCalledWith(60, 40);
104
- expect(context.rotate).toHaveBeenCalledWith(Math.PI / 2);
105
- expect(context.drawImage).toHaveBeenCalledWith(bitmap, -60, -40);
106
- expect(context.restore).toHaveBeenCalledTimes(1);
107
- });
108
-
109
- it("applies filters and uses quality for JPEG output", async () => {
110
- const { result } = renderHook(() => useImageProcessor());
111
-
112
- await result.current.filter(validJpeg, { brightness: 120, contrast: 90 });
113
-
114
- expect(context.filter).toBe("brightness(120%) contrast(90%)");
115
- expect(context.drawImage).toHaveBeenCalledWith(bitmap, 0, 0);
116
- expect(HTMLCanvasElement.prototype.toDataURL).toHaveBeenCalledWith(
117
- "image/jpeg",
118
- 0.85,
119
- );
120
- });
121
-
122
- it("uses quality for WebP output", async () => {
123
- const { result } = renderHook(() => useImageProcessor());
124
- const webp = "data:image/webp;base64,aGVsbG8=";
125
-
126
- await result.current.resize(webp, 60, 40);
127
-
128
- expect(HTMLCanvasElement.prototype.toDataURL).toHaveBeenCalledWith(
129
- "image/webp",
130
- 0.85,
131
- );
132
- });
133
-
134
- it("processes GIF input as PNG", async () => {
135
- const { result } = renderHook(() => useImageProcessor());
136
- const gif = "data:image/gif;base64,aGVsbG8=";
137
-
138
- await result.current.resize(gif, 60, 40);
139
-
140
- expect(createImageBitmap).toHaveBeenCalledWith(
141
- expect.objectContaining({ type: "image/png" }),
142
- );
143
- expect(HTMLCanvasElement.prototype.toDataURL).toHaveBeenCalledWith(
144
- "image/png",
145
- );
146
- });
147
- });
@@ -1,225 +0,0 @@
1
- import { useCallback, useRef } from "react";
2
-
3
- export interface ImageProcessor {
4
- resize: (base64Str: string, width: number, height: number) => Promise<string>;
5
- crop: (
6
- base64Str: string,
7
- x: number,
8
- y: number,
9
- width: number,
10
- height: number,
11
- ) => Promise<string>;
12
- flip: (
13
- base64Str: string,
14
- horizontal: boolean,
15
- vertical: boolean,
16
- ) => Promise<string>;
17
- rotate: (base64Str: string, angle: number) => Promise<string>;
18
- filter: (
19
- base64Str: string,
20
- filters: Record<string, number>,
21
- ) => Promise<string>;
22
- }
23
-
24
- export const useImageProcessor = (): ImageProcessor => {
25
- const canvasRef = useRef<HTMLCanvasElement | null>(null);
26
-
27
- const parseBase64 = useCallback((base64Str: string) => {
28
- const match = base64Str.match(/^data:(image\/[a-zA-Z+.-]+);base64,(.+)$/);
29
- if (!match) {
30
- throw new Error("Invalid Base64 image format");
31
- }
32
-
33
- let mimeType = match[1];
34
- const rawData = match[2];
35
-
36
- if (mimeType === "image/gif") {
37
- mimeType = "image/png";
38
- }
39
-
40
- return { mimeType, rawData };
41
- }, []);
42
-
43
- const getBitmap = useCallback(
44
- async (
45
- base64Str: string,
46
- ): Promise<{ bitmap: ImageBitmap; mimeType: string }> => {
47
- const { mimeType, rawData } = parseBase64(base64Str);
48
-
49
- const binaryStr = atob(rawData);
50
- const bytes = new Uint8Array(binaryStr.length);
51
- for (let i = 0; i < binaryStr.length; i++) {
52
- bytes[i] = binaryStr.charCodeAt(i);
53
- }
54
-
55
- const blob = new Blob([bytes], { type: mimeType });
56
- const bitmap = await createImageBitmap(blob);
57
-
58
- return { bitmap, mimeType };
59
- },
60
- [parseBase64],
61
- );
62
-
63
- const getContext = useCallback((mimeType: string) => {
64
- if (!canvasRef.current) {
65
- canvasRef.current = document.createElement("canvas");
66
- }
67
- const canvas = canvasRef.current;
68
-
69
- // Enable alpha channel for formats that support transparency
70
- const requiresAlpha = mimeType === "image/png" || mimeType === "image/webp";
71
- const ctx = canvas.getContext("2d", { alpha: requiresAlpha });
72
-
73
- return { canvas, ctx };
74
- }, []);
75
-
76
- const finalizeCanvas = useCallback(
77
- (canvas: HTMLCanvasElement, mimeType: string): string => {
78
- // PNG format does not support quality parameter
79
- if (mimeType === "image/jpeg" || mimeType === "image/webp") {
80
- return canvas.toDataURL(mimeType, 0.85);
81
- }
82
- return canvas.toDataURL(mimeType); // for PNG
83
- },
84
- [],
85
- );
86
-
87
- // 1. RESIZE
88
- const resize = useCallback(
89
- async (
90
- base64Str: string,
91
- width: number,
92
- height: number,
93
- ): Promise<string> => {
94
- const { bitmap, mimeType } = await getBitmap(base64Str);
95
- const { canvas, ctx } = getContext(mimeType);
96
-
97
- canvas.width = width;
98
- canvas.height = height;
99
-
100
- if (ctx) {
101
- ctx.clearRect(0, 0, width, height);
102
- ctx.drawImage(bitmap, 0, 0, width, height);
103
- }
104
-
105
- bitmap.close();
106
- return finalizeCanvas(canvas, mimeType);
107
- },
108
- [getBitmap, getContext, finalizeCanvas],
109
- );
110
-
111
- // 2. CROP
112
- const crop = useCallback(
113
- async (
114
- base64Str: string,
115
- x: number,
116
- y: number,
117
- width: number,
118
- height: number,
119
- ): Promise<string> => {
120
- const { bitmap, mimeType } = await getBitmap(base64Str);
121
- const { canvas, ctx } = getContext(mimeType);
122
-
123
- canvas.width = width;
124
- canvas.height = height;
125
-
126
- if (ctx) {
127
- ctx.clearRect(0, 0, width, height);
128
- ctx.drawImage(bitmap, x, y, width, height, 0, 0, width, height);
129
- }
130
-
131
- bitmap.close();
132
- return finalizeCanvas(canvas, mimeType);
133
- },
134
- [getBitmap, getContext, finalizeCanvas],
135
- );
136
-
137
- // 3. FLIP
138
- const flip = useCallback(
139
- async (
140
- base64Str: string,
141
- horizontal: boolean,
142
- vertical: boolean,
143
- ): Promise<string> => {
144
- const { bitmap, mimeType } = await getBitmap(base64Str);
145
- const { canvas, ctx } = getContext(mimeType);
146
-
147
- canvas.width = bitmap.width;
148
- canvas.height = bitmap.height;
149
-
150
- if (ctx) {
151
- ctx.clearRect(0, 0, canvas.width, canvas.height);
152
- ctx.save();
153
- ctx.translate(
154
- horizontal ? bitmap.width : 0,
155
- vertical ? bitmap.height : 0,
156
- );
157
- ctx.scale(horizontal ? -1 : 1, vertical ? -1 : 1);
158
- ctx.drawImage(bitmap, 0, 0);
159
- ctx.restore();
160
- }
161
-
162
- bitmap.close();
163
- return finalizeCanvas(canvas, mimeType);
164
- },
165
- [getBitmap, getContext, finalizeCanvas],
166
- );
167
-
168
- // 4. Rotate
169
- const rotate = useCallback(
170
- async (base64Str: string, angle: number): Promise<string> => {
171
- const { bitmap, mimeType } = await getBitmap(base64Str);
172
- const { canvas, ctx } = getContext(mimeType);
173
-
174
- canvas.width = bitmap.width;
175
- canvas.height = bitmap.height;
176
-
177
- if (ctx) {
178
- ctx.clearRect(0, 0, canvas.width, canvas.height);
179
- ctx.save();
180
- ctx.translate(bitmap.width / 2, bitmap.height / 2);
181
- ctx.rotate((angle * Math.PI) / 180);
182
- ctx.drawImage(bitmap, -bitmap.width / 2, -bitmap.height / 2);
183
- ctx.restore();
184
- }
185
-
186
- bitmap.close();
187
- return finalizeCanvas(canvas, mimeType);
188
- },
189
- [getBitmap, getContext, finalizeCanvas],
190
- );
191
-
192
- // 5. Filter
193
- const filter = useCallback(
194
- async (
195
- base64Str: string,
196
- filters: Record<string, number>,
197
- ): Promise<string> => {
198
- const { bitmap, mimeType } = await getBitmap(base64Str);
199
- const { canvas, ctx } = getContext(mimeType);
200
-
201
- canvas.width = bitmap.width;
202
- canvas.height = bitmap.height;
203
-
204
- if (ctx) {
205
- const filterString = Object.entries(filters)
206
- .map(([name, value]) => `${name}(${value}%)`)
207
- .join(" ");
208
- ctx.filter = filterString;
209
- ctx.drawImage(bitmap, 0, 0);
210
- }
211
-
212
- bitmap.close();
213
- return finalizeCanvas(canvas, mimeType);
214
- },
215
- [getBitmap, getContext, finalizeCanvas],
216
- );
217
-
218
- return {
219
- resize,
220
- crop,
221
- flip,
222
- rotate,
223
- filter,
224
- };
225
- };
@@ -1,33 +0,0 @@
1
- import { useState } from "react";
2
-
3
- type MobileDevice =
4
- | "android"
5
- | "ios"
6
- | "windows-phone"
7
- | "blackberry"
8
- | "opera-mini"
9
- | "mobile"
10
- | null;
11
-
12
- const detectMobileDevice = (): MobileDevice => {
13
- if (typeof window === "undefined" || !window.navigator) {
14
- return null;
15
- }
16
-
17
- const ua = window.navigator.userAgent.toLowerCase();
18
-
19
- if (/android/.test(ua)) return "android";
20
- if (/iphone|ipad|ipod/.test(ua)) return "ios";
21
- if (/windows phone/.test(ua)) return "windows-phone";
22
- if (/blackberry|bb10/.test(ua)) return "blackberry";
23
- if (/opera mini/.test(ua)) return "opera-mini";
24
- if (/mobile/.test(ua)) return "mobile";
25
-
26
- return null;
27
- };
28
-
29
- export const useMobile = (): MobileDevice => {
30
- const [mobile] = useState<MobileDevice>(() => detectMobileDevice());
31
-
32
- return mobile;
33
- };