@searpent/react-image-annotate 2.0.1 → 2.0.4

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.
@@ -6,6 +6,7 @@ import type {
6
6
  MainLayoutState,
7
7
  Mode,
8
8
  ToolEnum,
9
+ Metadata
9
10
  } from "../MainLayout/types"
10
11
  import React, { useEffect, useReducer } from "react"
11
12
  import makeImmutable, { without } from "seamless-immutable"
@@ -56,6 +57,18 @@ hideHeader ?: boolean,
56
57
  hideFullScreen ?: boolean,
57
58
  hideSave ?: boolean,
58
59
  onImagesChange ?: (any) => any,
60
+ groups ?: Array < any >,
61
+ onGroupSelect ?: (any) => any,
62
+ hideHistory ?: boolean,
63
+ hideNotEditingLabel ?: boolean,
64
+ showEditor ?: boolean,
65
+ showPageSelector ?: boolean,
66
+ clsColors ?: Object,
67
+ groupColors ?: Object,
68
+ onRecalc ?: (any) => any,
69
+ onSave ?: (any) => any,
70
+ allowedGroups ?: Object,
71
+ metadata ?: Array < Metadata >,
59
72
  }
60
73
 
61
74
  export const Annotator = ({
@@ -101,6 +114,18 @@ export const Annotator = ({
101
114
  hideSave,
102
115
  allowComments,
103
116
  onImagesChange,
117
+ groups,
118
+ onGroupSelect,
119
+ hideHistory,
120
+ hideNotEditingLabel,
121
+ showEditor,
122
+ showPageSelector,
123
+ clsColors,
124
+ groupColors,
125
+ onRecalc,
126
+ onSave,
127
+ allowedGroups,
128
+ metadata
104
129
  }: Props) => {
105
130
  if (typeof selectedImage === "string") {
106
131
  selectedImage = (images || []).findIndex((img) => img.src === selectedImage)
@@ -148,6 +173,9 @@ export const Annotator = ({
148
173
  videoSrc,
149
174
  keyframes,
150
175
  }),
176
+ imagesUpdatedAt: null,
177
+ imagesSavedAt: null,
178
+ metadata,
151
179
  })
152
180
  )
153
181
 
@@ -171,23 +199,63 @@ export const Annotator = ({
171
199
  })
172
200
  })
173
201
 
202
+ const handleSaveClick = async (e) => {
203
+ e.preventDefault()
204
+ if (onSave) {
205
+ onSave()
206
+ dispatchToReducer({
207
+ type: "IMAGES_SAVED",
208
+ savedAt: new Date()
209
+ })
210
+ }
211
+ }
212
+
213
+ const handleRecalcClick = (e) => {
214
+ e.preventDefault()
215
+ if (onRecalc) {
216
+ onRecalc()
217
+ }
218
+ }
219
+
220
+ const handleMetadataChange = ({ name, value, imageIndex }) => {
221
+ dispatchToReducer({
222
+ type: "UPDATE_METADATA",
223
+ name,
224
+ value,
225
+ imageIndex
226
+ })
227
+ }
228
+
229
+ // trigger this on every BBox manipulation (there is currently no way to detect adding of new box!)
174
230
  useEffect(() => {
231
+ if (!state.lastAction || !["BEGIN_BOX_TRANSFORM", "CHANGE_REGION", "DELETE_REGION"].includes(state.lastAction.type)) { return }
175
232
  if (onImagesChange) {
176
- onImagesChange({ selectedImage, images: state.images })
233
+ onImagesChange(state.images)
177
234
  }
235
+ dispatchToReducer({
236
+ type: "IMAGES_UPDATED",
237
+ updatedAt: new Date()
238
+ })
239
+ }, [onImagesChange, state.images, state.lastAction])
240
+
241
+ useEffect(() => {
178
242
  if (selectedImage === undefined) return
179
243
  dispatchToReducer({
180
244
  type: "SELECT_IMAGE",
181
245
  imageIndex: selectedImage,
182
246
  image: state.images[selectedImage],
183
247
  })
184
- }, [onImagesChange, selectedImage, state.images])
248
+ // eslint-disable-next-line react-hooks/exhaustive-deps
249
+ }, [onImagesChange, selectedImage])
185
250
 
186
251
  if (!images && !videoSrc)
187
252
  return 'Missing required prop "images" or "videoSrc"'
188
253
 
254
+
255
+ const [recalcActive, saveActive] = state.imagesSavedAt < state.imagesUpdatedAt ? [true, true] : [false, false];
256
+
189
257
  return (
190
- <SettingsProvider>
258
+ <SettingsProvider clsColors={clsColors} groupColors={groupColors}>
191
259
  <MainLayout
192
260
  RegionEditLabel={RegionEditLabel}
193
261
  alwaysShowNextButton={Boolean(onNextImage)}
@@ -203,6 +271,18 @@ export const Annotator = ({
203
271
  hideSettings={hideSettings}
204
272
  hideFullScreen={hideFullScreen}
205
273
  hideSave={hideSave}
274
+ groups={groups}
275
+ onGroupSelect={onGroupSelect}
276
+ hideHistory={hideHistory}
277
+ hideNotEditingLabel={hideNotEditingLabel}
278
+ showEditor={showEditor}
279
+ showPageSelector={showPageSelector}
280
+ onRecalc={handleRecalcClick}
281
+ onSave={handleSaveClick}
282
+ saveActive={recalcActive}
283
+ recalcActive={saveActive}
284
+ allowedGroups={allowedGroups}
285
+ onMetadataChange={handleMetadataChange}
206
286
  />
207
287
  </SettingsProvider>
208
288
  )
@@ -12,7 +12,9 @@ import { defaultKeyMap } from "../ShortcutsManager"
12
12
 
13
13
  import Annotator from "./"
14
14
 
15
- import { testRegions } from "../ImageCanvas/index.story"
15
+ import { testRegions, testRegionsBoxes } from "../ImageCanvas/index.story"
16
+ import photosToImages from "../utils/photosToImages"
17
+ import examplePhotos from "./examplePhotos"
16
18
 
17
19
  const middlewares = [
18
20
  (store) => (next) => (action) => {
@@ -54,33 +56,80 @@ storiesOf("Annotator", module)
54
56
  ))
55
57
  .add("Basic with onImagesChange", () => (
56
58
  <Annotator
57
- onExit={actionAddon("onExit")}
58
59
  middlewares={middlewares}
59
60
  labelImages
60
- regionClsList={["Alpha", "Beta", "Charlie", "Delta"]}
61
- regionTagList={["tag1", "tag2", "tag3"]}
62
- imageClsList={["Alpha", "Beta", "Charlie", "Delta"]}
63
- imageTagList={["tag1", "tag2", "tag3"]}
64
- onImagesChange={(images) => console.log("images changed to:", images)}
65
- images={[
66
- {
67
- src: exampleImage,
68
- name: "Seve's Desk",
69
- regions: testRegions,
70
- },
71
- {
72
- src: "https://loremflickr.com/100/100/cars?lock=1",
73
- name: "Frame 0036",
74
- },
75
- {
76
- src: "https://loremflickr.com/100/100/cars?lock=2",
77
- name: "Frame 0037",
78
- },
79
- {
80
- src: "https://loremflickr.com/100/100/cars?lock=3",
81
- name: "Frame 0038",
82
- },
61
+ regionClsList={[
62
+ "author",
63
+ "appendix",
64
+ "photo_author",
65
+ "photo_caption",
66
+ "advertisement",
67
+ "other_graphics",
68
+ "unknown",
69
+ "title",
70
+ "about_author",
71
+ "image",
72
+ "subtitle",
73
+ "interview",
74
+ "table",
75
+ "text",
83
76
  ]}
77
+ allowedGroups={[
78
+ { value: '0', label: '0' },
79
+ { value: '1', label: '1' },
80
+ { value: '2', label: '2' },
81
+ { value: '3', label: '3' },
82
+ { value: '4', label: '4' },
83
+ { value: '5', label: '5' },
84
+ { value: '6', label: '6' },
85
+ { value: '7', label: '7' },
86
+ { value: '8', label: '8' },
87
+ { value: '9', label: '9' },
88
+ ]}
89
+ onImagesChange={(images) => console.log("[images changed to]:", images)}
90
+ images={photosToImages([...examplePhotos, ...examplePhotos, ...examplePhotos])}
91
+ clsColors={{
92
+ title: "#f70202",
93
+ subtitle: "#ffb405",
94
+ text: "#14deef",
95
+ author: "#f8d51e",
96
+ appendix: "#bfede2",
97
+ photo_author: "#9a17bb",
98
+ photo_caption: "#ff84f6",
99
+ advertisement: "#ffb201",
100
+ other_graphics: "#ff5400",
101
+ unknown: "#bfede2",
102
+ about_author: "#9a17bb",
103
+ image: "#14deef",
104
+ interview: "#23b20f",
105
+ table: "#02b4ba",
106
+ }}
107
+ groupColors={{
108
+ "0": "#e3a7c0",
109
+ "1": "#c2d5a8",
110
+ "2": "#f2e9cc",
111
+ "3": "#bad5f0",
112
+ "4": "#f0d5ba",
113
+ "5": "#d6eff5",
114
+ "6": "#f8d7e8",
115
+ "7": "#a5d5d5",
116
+ "8": "#b0abcb",
117
+ "9": "#fae4cc",
118
+ }}
119
+ onGroupSelect={(groupId) => console.log('selected groupid:', groupId)}
120
+ hideHeader={true}
121
+ hideHistory={true}
122
+ hideNotEditingLabel={true}
123
+ showEditor={true}
124
+ showPageSelector={true}
125
+ metadata={[{
126
+ key: "name", value: "Dennik Aha"
127
+ }, {
128
+ key: "released", value: "20/1/2022"
129
+ }]}
130
+ onSave={() => console.log("[onSave] triggered]")}
131
+ onRecalc={() => console.log("[onRecalc] triggered]")}
132
+ onExit={(s) => console.log('[onExit] triggered:', s)}
84
133
  />
85
134
  ))
86
135
  .add("Basic - Allow Comments", () => (
@@ -12,6 +12,7 @@ import convertExpandingLineToPolygon from "./convert-expanding-line-to-polygon"
12
12
  import clamp from "clamp"
13
13
  import getLandmarksWithTransform from "../../utils/get-landmarks-with-transform"
14
14
  import setInLocalStorage from "../../utils/set-in-local-storage"
15
+ import onlyUnique from "../../utils/filter-only-unique"
15
16
 
16
17
  const getRandomId = () => Math.random().toString().split(".")[1]
17
18
 
@@ -167,8 +168,17 @@ export default (state: MainLayoutState, action: Action) => {
167
168
  const regions = [...(activeImage.regions || [])].map((r) => ({
168
169
  ...r,
169
170
  highlighted: r.id === region.id,
171
+ groupHighlighted: (r.groupId && r.groupId === region.groupId) ? true : false,
170
172
  editingLabels: r.id === region.id,
171
173
  }))
174
+
175
+ const selectedGroupIds = regions.filter(i => i.highlighted).map(r => r.groupId || '').filter(onlyUnique);
176
+ if (selectedGroupIds.length === 1) {
177
+ state = setIn(state, [...pathToActiveImage, "selectedGroupId"], selectedGroupIds[0])
178
+ }
179
+ if (selectedGroupIds.length === 0) {
180
+ state = setIn(state, [...pathToActiveImage, "selectedGroupId"], null)
181
+ }
172
182
  return setIn(state, [...pathToActiveImage, "regions"], regions)
173
183
  }
174
184
  case "BEGIN_MOVE_POINT": {
@@ -310,15 +320,15 @@ export default (state: MainLayoutState, action: Action) => {
310
320
  xFree === 0
311
321
  ? ow
312
322
  : xFree === -1
313
- ? ow + (ox - dx)
314
- : Math.max(0, ow + (x - ox - ow))
323
+ ? ow + (ox - dx)
324
+ : Math.max(0, ow + (x - ox - ow))
315
325
  const dy = yFree === 0 ? oy : yFree === -1 ? Math.min(oy + oh, y) : oy
316
326
  const dh =
317
327
  yFree === 0
318
328
  ? oh
319
329
  : yFree === -1
320
- ? oh + (oy - dy)
321
- : Math.max(0, oh + (y - oy - oh))
330
+ ? oh + (oy - dy)
331
+ : Math.max(0, oh + (y - oy - oh))
322
332
 
323
333
  // determine if we should switch the freedom
324
334
  if (dw <= 0.001) {
@@ -636,18 +646,18 @@ export default (state: MainLayoutState, action: Action) => {
636
646
  const [[keypointsDefinitionId, { landmarks, connections }]] =
637
647
  (Object.entries(state.keypointDefinitions): any)
638
648
 
639
- newRegion = {
640
- type: "keypoints",
641
- keypointsDefinitionId,
642
- points: getLandmarksWithTransform({
643
- landmarks,
644
- center: { x, y },
645
- scale: 1,
646
- }),
647
- highlighted: true,
648
- editingLabels: false,
649
- id: getRandomId(),
650
- }
649
+ newRegion = {
650
+ type: "keypoints",
651
+ keypointsDefinitionId,
652
+ points: getLandmarksWithTransform({
653
+ landmarks,
654
+ center: { x, y },
655
+ scale: 1,
656
+ }),
657
+ highlighted: true,
658
+ editingLabels: false,
659
+ id: getRandomId(),
660
+ }
651
661
  state = setIn(state, ["mode"], {
652
662
  mode: "RESIZE_KEYPOINTS",
653
663
  landmarks,
@@ -664,10 +674,12 @@ export default (state: MainLayoutState, action: Action) => {
664
674
 
665
675
  const regions = [...(getIn(state, pathToActiveImage).regions || [])]
666
676
  .map((r) =>
667
- setIn(r, ["editingLabels"], false).setIn(["highlighted"], false)
677
+ setIn(r, ["editingLabels"], false).setIn(["highlighted"], false).setIn(["groupHighlighted"], false)
668
678
  )
669
679
  .concat(newRegion ? [newRegion] : [])
670
680
 
681
+ state = setIn(state, [...pathToActiveImage, "selectedGroupId"], null)
682
+
671
683
  return setIn(state, [...pathToActiveImage, "regions"], regions)
672
684
  }
673
685
  case "MOUSE_UP": {
@@ -907,6 +919,76 @@ export default (state: MainLayoutState, action: Action) => {
907
919
  }
908
920
  break
909
921
  }
922
+ case "UPDATE_REGIONS": {
923
+ const { imageIndex, regions: newRegions } = action;
924
+ const updatedRegions = state.images[imageIndex].regions.map(r => {
925
+ const updatedRegion = newRegions.find(i => i.id === r.id)
926
+ if (!updatedRegion) {
927
+ return r
928
+ }
929
+ return {
930
+ ...r,
931
+ cls: updatedRegion.cls,
932
+ text: updatedRegion.text
933
+ }
934
+ })
935
+ // TODO: add mutation of order and deletion of regions - SI-1967
936
+ return setIn(
937
+ state,
938
+ ["images", imageIndex, "regions"],
939
+ updatedRegions
940
+ )
941
+ }
942
+ case "IMAGES_UPDATED": {
943
+ const { updatedAt } = action;
944
+ return setIn(
945
+ state,
946
+ ["imagesUpdatedAt"],
947
+ updatedAt
948
+ )
949
+ }
950
+ case "IMAGES_SAVED": {
951
+ const { savedAt } = action;
952
+ return setIn(
953
+ state,
954
+ ["imagesSavedAt"],
955
+ savedAt
956
+ )
957
+ }
958
+ case "UPDATE_METADATA": {
959
+ const { name, value, imageIndex } = action;
960
+ if (isNaN(imageIndex)) {
961
+ // update global metadata
962
+ const metadataIndex = state.metadata?.findIndex(mt => mt.key === name)
963
+ if (metadataIndex < 0) {
964
+ console.error(`can't find metadata by key "${name}"`)
965
+ return
966
+ }
967
+ return setIn(
968
+ state,
969
+ ["metadata", metadataIndex],
970
+ {
971
+ key: name,
972
+ value: value
973
+ }
974
+ )
975
+ } else {
976
+ // update local metadata of imageIndex
977
+ const metadataIndex = state.images[imageIndex]?.metadata?.findIndex(mt => mt.key === name)
978
+ if (metadataIndex < 0) {
979
+ console.error(`can't find metadata by key "${name}"`)
980
+ return
981
+ }
982
+ return setIn(
983
+ state,
984
+ ["images", imageIndex, "metadata", metadataIndex],
985
+ {
986
+ key: name,
987
+ value: value
988
+ }
989
+ )
990
+ }
991
+ }
910
992
  default:
911
993
  break
912
994
  }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Plugin styles
3
+ */
4
+ .ce-header {
5
+ padding: 0.6em 0 3px;
6
+ margin: 0;
7
+ line-height: 1.25em;
8
+ outline: none;
9
+ }
10
+
11
+ .ce-header p,
12
+ .ce-header div {
13
+ padding: 0 !important;
14
+ margin: 0 !important;
15
+ }
16
+
17
+ /**
18
+ * Styles for Plugin icon in Toolbar
19
+ */
20
+ .ce-header__icon {}
21
+
22
+ .ce-header[contentEditable=true][data-placeholder]::before {
23
+ position: absolute;
24
+ content: attr(data-placeholder);
25
+ color: #707684;
26
+ font-weight: normal;
27
+ display: none;
28
+ cursor: text;
29
+ }
30
+
31
+ .ce-header[contentEditable=true][data-placeholder]:empty::before {
32
+ display: block;
33
+ }
34
+
35
+ .ce-header[contentEditable=true][data-placeholder]:empty:focus::before {
36
+ display: none;
37
+ }
38
+
39
+ /* Custom overwrite */
40
+ .cdx-settings-button {
41
+ width: 100% !important;
42
+ }
43
+
44
+ .ce-settings__plugin-zone {
45
+ padding: 0 .25rem;
46
+ }