이정민

Merge branch 'develop'

...@@ -26,4 +26,7 @@ out ...@@ -26,4 +26,7 @@ out
26 *.bak 26 *.bak
27 27
28 node_modules.nosync/ 28 node_modules.nosync/
29 -*.env.*
...\ No newline at end of file ...\ No newline at end of file
29 +*.env.*
30 +
31 +package-lock.json
32 +yarn.lock
...\ No newline at end of file ...\ No newline at end of file
......
This diff could not be displayed because it is too large.
1 -import GIF from "gifencoder";
2 -
3 -class GifGenerator {
4 - constructor(canvas) {
5 - this.canvas = canvas;
6 - this.width = canvas.getWidth();
7 - this.height = canvas.getHeight();
8 - this.gif = new GIF(this.width, this.height);
9 -
10 - this.gif.start();
11 - this.gif.setTransparent(null);
12 - this.gif.setRepeat(0);
13 - this.gif.setQuality(10);
14 - }
15 -
16 - addFrame(delay = 0) {
17 - this.gif.setDelay(delay);
18 - this.gif.addFrame(this.canvas.getContext());
19 - }
20 -
21 - render() {
22 - this.gif.finish();
23 - const byte = new Uint8Array(this.gif.out.data);
24 -
25 - return new Blob([byte], { type: "image/gif" });
26 - }
27 -}
28 -
29 -window.GifGenerator = GifGenerator;
1 +const aws = require('aws-sdk');
2 +const s3 = new aws.S3();
3 +
4 +exports.handler = async (event) => {
5 + if(!event['queryStringParameters'] || !event['queryStringParameters']['id']) {
6 + return {
7 + statusCode: 400
8 + }
9 + }
10 +
11 + const id = event['queryStringParameters']['id'];
12 + const data = await download(id);
13 +
14 + return {
15 + statusCode: 200,
16 + headers:{
17 + "Content-Type":"image/gif"
18 + },
19 + isBase64Encoded:true,
20 + body: data.Body.toString("base64")
21 + }
22 +};
23 +
24 +const download = (id) => {
25 + const bucket = 'gif-generator';
26 + const path = `/gif/${id}.gif`;
27 + const params = {
28 + Bucket: bucket,
29 + Key: path
30 + }
31 + return new Promise((resolve, reject) => {
32 + s3.getObject(params, (err, data) => {
33 + if(err){
34 + console.log("download err");
35 + console.log(err);
36 + reject(err);
37 + }else{
38 + console.log("download success");
39 + console.log(data);
40 + resolve(data);
41 + }
42 + });
43 + });
44 +}
...\ No newline at end of file ...\ No newline at end of file
1 +node_modules
2 +dist
...\ No newline at end of file ...\ No newline at end of file
1 +{
2 + "name": "gif-upload",
3 + "version": "1.0.0",
4 + "description": "",
5 + "main": "index.js",
6 + "scripts": {
7 + "test": "echo \"Error: no test specified\" && exit 1"
8 + },
9 + "author": "",
10 + "license": "ISC",
11 + "dependencies": {
12 + "aws-sdk": "^2.905.0",
13 + "busboy": "^0.3.1",
14 + "uuid-random": "^1.3.2"
15 + },
16 + "devDependencies": {
17 + "uglifyjs-webpack-plugin": "^2.2.0"
18 + }
19 +}
1 +AWS Lambda 업로드를 위해 사용하는 코드
2 +
3 +## bundle
4 +webpack
5 +
6 +## usage
7 +aws lambda에서는 npm을 사용할 수 없기 때문에 사용할 npm 모듈을 미리 로드하여 bundle.
8 +
9 +사용할 npm 모듈을 import에 넣어 webpack을 이용해 bundle한 후 aws lambda에 import.js 파일 업로드
10 +
11 +이후 index.js에서 import.js를 불러와 사용
...\ No newline at end of file ...\ No newline at end of file
1 +module.exports = {
2 + Busboy:require('busboy'),
3 + UUID:require('uuid-random')
4 +}
...\ No newline at end of file ...\ No newline at end of file
1 +const imports = require('./import')['import'];
2 +
3 +const Busboy = imports.Busboy;
4 +const UUID = imports.UUID;
5 +const aws = require('aws-sdk');
6 +const s3 = new aws.S3();
7 +
8 +exports.handler = async (event, context) => {
9 + if(!event.body || !/^multipart\/form\-data/.test(event.headers['content-type'])) {
10 + return {
11 + statusCode: 400
12 + }
13 + }
14 +
15 + const formData = await parse(event);
16 +
17 + if(!formData['gif']) {
18 + return {
19 + statusCode: 400
20 + }
21 + }
22 +
23 + const id = await upload(formData['gif']);
24 + return {
25 + statusCode: 200,
26 + headers:{
27 + "Content-Type":"json"
28 + },
29 + body: JSON.stringify({
30 + id
31 + }),
32 + }
33 +}
34 +
35 +const parse = (event) => new Promise((resolve, reject) => {
36 + const bodyBuffer = new Buffer(event.body.toString(), "base64");
37 +
38 + const busboy = new Busboy({
39 + headers: {
40 + 'content-type': event.headers['content-type']
41 + }
42 + });
43 + const formData = {};
44 +
45 + busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
46 + console.log('File [%s]: filename=%j; encoding=%j; mimetype=%j', fieldname, filename, encoding, mimetype);
47 + const chunks = [];
48 +
49 + file.on('data', data => {
50 + chunks.push(data);
51 + }).on('end', () => {
52 + formData[fieldname] = {
53 + name:filename,
54 + data:Buffer.concat(chunks),
55 + mimetype:mimetype
56 + };
57 + console.log("File [%s] finished.", filename);
58 + });
59 + });
60 +
61 + busboy.on('field', (fieldname, value) => {
62 + console.log("[" + fieldname + "] >> " + value);
63 + formData[fieldname] = value;
64 + });
65 +
66 + busboy.on('error', error => {
67 + reject(error);
68 + });
69 +
70 + busboy.on('finish', () => {
71 + resolve(formData);
72 + });
73 +
74 + busboy.write(bodyBuffer, event.isBase64Encoded ? 'base64' : 'binary');
75 + busboy.end();
76 +});
77 +
78 +const upload = ({data, mimetype}) => new Promise((resolve, reject) => {
79 + const bucket = 'gif-generator';
80 + const path = '/gif';
81 + const id = UUID().replace(/\-/g, '');
82 + const fileFullName = path + '/' + id + '.gif';
83 + const params = {
84 + Bucket: bucket,
85 + Key: fileFullName,
86 + Body: data,
87 + ContentType: mimetype
88 + };
89 +
90 + s3.upload(params, (err, data) => {
91 + if(err){
92 + console.log("upload err");
93 + console.log(err);
94 + reject(err);
95 + }else{
96 + console.log("upload success");
97 + console.log(data);
98 + resolve(id);
99 + }
100 + });
101 +});
...\ No newline at end of file ...\ No newline at end of file
1 +const path = require('path');
2 +
3 +module.exports = {
4 + entry: './src/import.js',
5 + output: {
6 + path: __dirname + '/dist',
7 + filename: 'import.js',
8 + library: 'import',
9 + libraryTarget: 'commonjs2'
10 + },
11 + module: {
12 + rules: [
13 + {
14 + test: /\.js$/,
15 + include: [
16 + path.resolve(__dirname, 'src/js')
17 + ],
18 + exclude: /node_modules/
19 + }
20 + ]
21 + },
22 + mode: 'development',
23 + devtool:false,
24 + resolve: {
25 + fallback: {
26 + "fs":false,
27 + "stream": require.resolve("stream-browserify")
28 + }
29 + }
30 +};
...\ No newline at end of file ...\ No newline at end of file
1 +# AWS Lambda API
2 +[https://9davbjzey4.execute-api.ap-northeast-2.amazonaws.com](https://9davbjzey4.execute-api.ap-northeast-2.amazonaws.com)
3 +
4 +## gif upload
5 +### request
6 +- endpoint : /
7 +- Method : POST
8 +- Content-Type : multipart/form-data
9 +- Body : gif=`<file>`
10 +
11 +### response
12 +- 200 : { id: `<id>` }
13 +- 400 : no gif
14 +- 500 : server error
15 +
16 +## gif download
17 +### request
18 +- endpoint : /
19 +- Method : GET
20 +- QueryString : id=`<id>`
21 +
22 +### response
23 +- 200 : Content-type:image/gif
24 +- 400 : no id
25 +- 404 : not found
26 +- 500 : server error
...\ No newline at end of file ...\ No newline at end of file
...@@ -7,13 +7,16 @@ ...@@ -7,13 +7,16 @@
7 "@testing-library/jest-dom": "^5.11.4", 7 "@testing-library/jest-dom": "^5.11.4",
8 "@testing-library/react": "^11.1.0", 8 "@testing-library/react": "^11.1.0",
9 "@testing-library/user-event": "^12.1.10", 9 "@testing-library/user-event": "^12.1.10",
10 + "@toast-ui/react-image-editor": "3.14.2",
11 + "@types/fabric": "^4.2.5",
12 + "axios": "^0.21.1",
13 + "fabric": "^4.4.0",
10 "next": "^10.0.5", 14 "next": "^10.0.5",
11 "react": "^17.0.2", 15 "react": "^17.0.2",
12 "react-dom": "^17.0.2", 16 "react-dom": "^17.0.2",
13 "styled-components": "^5.2.3", 17 "styled-components": "^5.2.3",
14 "styled-reset": "^4.3.4", 18 "styled-reset": "^4.3.4",
15 "tui-image-editor": "3.14.2", 19 "tui-image-editor": "3.14.2",
16 - "@toast-ui/react-image-editor": "3.14.2",
17 "web-vitals": "^1.0.1" 20 "web-vitals": "^1.0.1"
18 }, 21 },
19 "devDependencies": { 22 "devDependencies": {
......
1 +import axios from "axios";
2 +
3 +const baseURL = "https://9davbjzey4.execute-api.ap-northeast-2.amazonaws.com";
4 +
5 +export const postGif = async (formData) => {
6 + console.log("file", formData);
7 + const { data } = await axios.post(baseURL, formData);
8 +
9 + return data;
10 +};
11 +
12 +export const getGif = async (id) => {
13 + const { data } = await axios.get(baseURL, {
14 + params: {
15 + id: id,
16 + },
17 + });
18 +
19 + return data;
20 +};
1 +import { useEffect, useRef, useState } from "react";
2 +import styled from "styled-components";
3 +import TuiImageEditor from "tui-image-editor";
4 +import "gif-generator/dist/gif-generator";
5 +import { postGif } from "api";
6 +
7 +declare global {
8 + interface Window {
9 + GifGenerator: any;
10 + }
11 +}
12 +
13 +const GifEditor = ({ previewURL }) => {
14 + const [imageEditor, setImageEditor] = useState(null);
15 +
16 + const rootEl = useRef();
17 +
18 + const [download, setDownload] = useState(null);
19 + const [blob, setBlob] = useState(null);
20 +
21 + const [isMakeStarted, setIsMakeStarted] = useState(false);
22 + const [isUploadLoading, setIsUploadLoading] = useState(false);
23 + const [viewLink, setViewLink] = useState(null);
24 +
25 + useEffect(() => {
26 + if (window) {
27 + setImageEditor(
28 + new TuiImageEditor(rootEl.current, {
29 + includeUI: {
30 + loadImage: {
31 + path: previewURL,
32 + name: "SampleImage",
33 + },
34 + uiSize: {
35 + width: "100%",
36 + height: "600px",
37 + },
38 + menu: ["draw", "text"],
39 + menuBarPosition: "bottom",
40 + },
41 + cssMaxWidth: 500,
42 + cssMaxHeight: 700,
43 + usageStatistics: false,
44 + })
45 + );
46 + }
47 + }, []);
48 +
49 + useEffect(() => {
50 + if (imageEditor) {
51 + console.log(imageEditor._graphics.getCanvas().getObjects());
52 + }
53 + }, [imageEditor]);
54 +
55 + const makeGif = () => {
56 + setIsMakeStarted(true);
57 + const gifGenerator = new window.GifGenerator(
58 + imageEditor._graphics.getCanvas()
59 + );
60 + gifGenerator.make().then(
61 + (blob) => {
62 + setBlob(blob);
63 + setDownload(window.URL.createObjectURL(blob));
64 + },
65 + (error) => {
66 + alert(error);
67 + }
68 + );
69 + };
70 +
71 + const handleUpload = async () => {
72 + setIsUploadLoading(true);
73 + const file = new File([blob], "new_gif.gif");
74 + const formData = new FormData();
75 + formData.append("gif", file);
76 + const res = await postGif(formData);
77 + console.log(res);
78 + setIsUploadLoading(false);
79 + setViewLink(
80 + `https://gif-generator.s3.ap-northeast-2.amazonaws.com//gif/${res.id}.gif`
81 + );
82 + };
83 +
84 + return (
85 + <>
86 + <Wrapper>
87 + {((isMakeStarted && !download) || isUploadLoading) && (
88 + <>
89 + <div className="background" />
90 + <div className="download">loading...</div>
91 + </>
92 + )}
93 + {!isUploadLoading && viewLink && (
94 + <div className="download" style={{ zIndex: 200 }}>
95 + <a href={viewLink}>{viewLink}</a>
96 + </div>
97 + )}
98 + {download && !isUploadLoading && (
99 + <>
100 + <div className="background" />
101 + <div className="download">
102 + <div className="download__btn">
103 + <a href={download} download="new_gif.gif">
104 + Download a File
105 + </a>
106 + </div>
107 + <div className="download__btn">
108 + <div onClick={handleUpload}>Upload to Server</div>
109 + </div>
110 + </div>
111 + </>
112 + )}
113 + <div onClick={makeGif} className="make">
114 + Make a Gif
115 + </div>
116 + <div ref={rootEl} />
117 + </Wrapper>
118 + </>
119 + );
120 +};
121 +
122 +const Wrapper = styled.div`
123 + position: fixed;
124 + width: 90%;
125 + top: 10rem;
126 + border-radius: 1.5rem;
127 + box-shadow: ${({ theme }) => theme.boxShadow.normal};
128 + display: flex;
129 + flex-direction: column;
130 + align-items: center;
131 + a {
132 + color: black;
133 + text-decoration: none;
134 + }
135 + .make {
136 + font: 800 11.5px Arial;
137 + position: absolute;
138 + right: 0;
139 + top: 0;
140 + width: 120px;
141 + height: 40px;
142 + background: red;
143 + z-index: 10;
144 + border-radius: 20px;
145 + margin: 8px;
146 + background-color: #fdba3b;
147 + display: flex;
148 + align-items: center;
149 + justify-content: center;
150 + cursor: pointer;
151 + }
152 + .background {
153 + position: fixed;
154 + top: 0;
155 + left: 0;
156 + width: 100%;
157 + height: 100vh;
158 + background-color: black;
159 + opacity: 0.7;
160 + z-index: 100;
161 + }
162 + .download {
163 + position: absolute;
164 + top: 15rem;
165 + z-index: 100;
166 + display: flex;
167 + background-color: white;
168 + padding: 1.5rem 2rem;
169 + border-radius: 2rem;
170 + &__btn {
171 + cursor: pointer;
172 + :last-child {
173 + margin-left: 1rem;
174 + }
175 + }
176 + }
177 + .tui-image-editor-container {
178 + border-radius: 1.5rem;
179 + }
180 + .tui-image-editor-container .tui-image-editor-help-menu.top {
181 + left: 19rem;
182 + top: 1rem;
183 + }
184 + .tui-image-editor-header-logo {
185 + display: none;
186 + }
187 + .tui-image-editor-header-buttons {
188 + display: none;
189 + }
190 +`;
191 +
192 +export default GifEditor;
1 -import dynamic from "next/dynamic";
2 -import { useState } from "react";
3 -import styled from "styled-components";
4 -
5 -const ToastEditor = dynamic(() => import("components/ToastEditor"), {
6 - ssr: false,
7 -});
8 -const Image = ({ previewURL, setPreviewURL }) => {
9 - const [file, setFile] = useState(undefined);
10 - console.log("previewURL", previewURL);
11 -
12 - // const uploadImage = (file) => {
13 - // if (!file) {
14 - // return;
15 - // }
16 - // };
17 -
18 - // const selectImg = (e) => {
19 - // const reader = new FileReader();
20 - // const targetFile = e.target.files[0];
21 - // setFile(targetFile);
22 - // // uploadImage(targetFile);
23 -
24 - // reader.onloadend = () => {
25 - // setPreviewURL(reader.result);
26 - // };
27 -
28 - // reader.readAsDataURL(targetFile);
29 - // };
30 -
31 - // const [isEditorOpened, setIsEditorOpened] = useState(false);
32 - // const handleEditor = () => {
33 - // setIsEditorOpened(true);
34 - // };
35 -
36 - return (
37 - <>
38 - <Container>
39 - <ImgBox>
40 - {/* <div onClick={handleEditor}>asdf</div> */}
41 - {/* {file === undefined ? ( */}
42 - <>
43 - {/* <div className="sub-flex">
44 - <BlankBox />
45 - <div>Click to add a photo</div>
46 - <input
47 - type="file"
48 - style={{
49 - position: "absolute",
50 - top: 0,
51 - paddingLeft: 0,
52 - zIndex: 0,
53 - width: "90%",
54 - height: "100%",
55 - border: "none",
56 - cursor: "pointer",
57 - outline: "none",
58 - }}
59 - onChange={selectImg}
60 - />
61 - </div>
62 - <div className="sub-flex">Open Image Editor</div> */}
63 - </>
64 - {/* ) : ( */}
65 - <img
66 - id="image"
67 - alt={""}
68 - style={{
69 - objectFit: "cover",
70 - display: "flex",
71 - maxHeight: "90%",
72 - maxWidth: "90%",
73 - }}
74 - src={previewURL as string}
75 - />
76 - {/* )} */}
77 - </ImgBox>
78 - {/* <Menu /> */}
79 - </Container>
80 - {/* {isEditorOpened && <ToastEditor {...{ setPreviewURL, setIsImgAdded }} />} */}
81 - </>
82 - );
83 -};
84 -
85 -const Menu = () => {
86 - return (
87 - <div style={{ width: "15rem", marginLeft: "2rem" }}>
88 - <Box />
89 - <Box />
90 - <Box />
91 - <Box />
92 - </div>
93 - );
94 -};
95 -
96 -const Container = styled.div`
97 - width: 100%;
98 - display: flex;
99 - justify-content: center;
100 - margin-top: 10rem;
101 -`;
102 -const ImgBox = styled.div`
103 - position: relative;
104 - width: 90%;
105 - /* height: 30rem; */
106 - background-color: white;
107 - box-shadow: ${({ theme }) => theme.boxShadow.normal};
108 - border-radius: 2rem;
109 - margin-top: 2rem;
110 - display: flex;
111 - align-items: center;
112 - justify-content: center;
113 - font-size: 1rem;
114 - display: flex;
115 - /* flex: 0.6; */
116 - padding: 1rem 0;
117 - /* .sub-flex {
118 - position: relative;
119 - width: 100%;
120 - height: 100%;
121 - display: flex;
122 - align-items: center;
123 - justify-content: center;
124 - :first-child {
125 - border-right: 1px solid ${({ theme }) => theme.color.gray};
126 - }
127 - } */
128 -`;
129 -
130 -const Box = styled.div`
131 - width: 100%;
132 - height: 10rem;
133 - margin-top: 2rem;
134 - border-radius: 1rem;
135 - background-color: white;
136 - box-shadow: ${({ theme }) => theme.boxShadow.normal};
137 -`;
138 -const BlankBox = styled.div`
139 - z-index: 1;
140 - position: absolute;
141 - top: 0;
142 - width: 90%;
143 - height: 50px;
144 - background-color: white;
145 -`;
146 -
147 -export default Image;
1 /// <reference path="react-image-editor.d.ts" /> 1 /// <reference path="react-image-editor.d.ts" />
2 import ImageEditor from "@toast-ui/react-image-editor"; 2 import ImageEditor from "@toast-ui/react-image-editor";
3 -import { useEffect, useState } from "react"; 3 +import { useState } from "react";
4 import styled from "styled-components"; 4 import styled from "styled-components";
5 import "tui-image-editor/dist/tui-image-editor.css"; 5 import "tui-image-editor/dist/tui-image-editor.css";
6 6
7 const ToastEditor = ({ setPreviewURL, setIsImgAdded, setIsEditorOpened }) => { 7 const ToastEditor = ({ setPreviewURL, setIsImgAdded, setIsEditorOpened }) => {
8 - // const [lowerCanvas, setLowerCanvas] = useState<HTMLCanvasElement>(); 8 + const [alertIsShown, setAlertIsShown] = useState(false);
9 - // const [upperCanvas, setUpperCanvas] = useState<HTMLCanvasElement>();
10 - // // console.log(
11 - // // document.getElementsByClassName("lower-canvas")[0]?.toDataURL("image/png")
12 - // // );
13 - // console.log("s");
14 -
15 - // // const [upperCanvas, setUpperCanvas] = useState(
16 - // // document.getElementsByClassName("upper-canvas ")[0]
17 - // // );
18 -
19 - // useEffect(() => {
20 - // window?.addEventListener("click", () => {
21 - // setLowerCanvas(
22 - // document.getElementsByClassName("lower-canvas")[0] as HTMLCanvasElement
23 - // );
24 - // setUpperCanvas(
25 - // document.getElementsByClassName("upper-canvas")[0] as HTMLCanvasElement
26 - // );
27 - // });
28 - // }, []);
29 -
30 - // useEffect(() => {
31 - // const img = lowerCanvas?.toDataURL("image/png");
32 - // const uploaded = document.getElementById("image");
33 - // console.log(uploaded);
34 - // // let w = window.open();
35 - // // if (w?.window) w.document.body.innerHTML = "<img src='" + img + "'>";
36 - // const image = new Image();
37 - // // image.onload = function () {
38 - // // lowerCanvas.width = uploaded.clientWidth;
39 - // // lowerCanvas.height = uploaded.clientHeight;
40 - // // lowerCanvas?.getContext("2d").drawImage(image, 0, 0);
41 - // // };
42 - // image.src = previewURL;
43 - // console.log("b");
44 - // if (lowerCanvas?.getContext&&upperCanvas?.getContext) {
45 - // image.onload = function () {
46 -
47 - // lowerCanvas.width = 1000;
48 - // lowerCanvas.height = 572;
49 - // upperCanvas.width = 1000;
50 - // upperCanvas.height = 572;
51 - // lowerCanvas?.getContext("2d").drawImage(image, 0, 0);
52 - // };
53 - // console.log(lowerCanvas.getContext("2d"));
54 - // }
55 - // }, [lowerCanvas?.toDataURL("image/png")]);
56 9
57 const handleEnd = () => { 10 const handleEnd = () => {
58 const lowerCanvas = document.getElementsByClassName( 11 const lowerCanvas = document.getElementsByClassName(
59 "lower-canvas" 12 "lower-canvas"
60 )[0] as HTMLCanvasElement; 13 )[0] as HTMLCanvasElement;
61 - setPreviewURL(lowerCanvas.toDataURL("image/png")); 14 + if (
62 - console.log("asdf"); 15 + lowerCanvas.toDataURL("image/png") ===
63 - setIsImgAdded(true); 16 + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAACWCAYAAABkW7XSAAAEYklEQVR4Xu3UAQkAAAwCwdm/9HI83BLIOdw5AgQIRAQWySkmAQIEzmB5AgIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlAABg+UHCBDICBisTFWCEiBgsPwAAQIZAYOVqUpQAgQMlh8gQCAjYLAyVQlKgIDB8gMECGQEDFamKkEJEDBYfoAAgYyAwcpUJSgBAgbLDxAgkBEwWJmqBCVAwGD5AQIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlAABg+UHCBDICBisTFWCEiBgsPwAAQIZAYOVqUpQAgQMlh8gQCAjYLAyVQlKgIDB8gMECGQEDFamKkEJEDBYfoAAgYyAwcpUJSgBAgbLDxAgkBEwWJmqBCVAwGD5AQIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlAABg+UHCBDICBisTFWCEiBgsPwAAQIZAYOVqUpQAgQMlh8gQCAjYLAyVQlKgIDB8gMECGQEDFamKkEJEDBYfoAAgYyAwcpUJSgBAgbLDxAgkBEwWJmqBCVAwGD5AQIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlAABg+UHCBDICBisTFWCEiBgsPwAAQIZAYOVqUpQAgQMlh8gQCAjYLAyVQlKgIDB8gMECGQEDFamKkEJEDBYfoAAgYyAwcpUJSgBAgbLDxAgkBEwWJmqBCVAwGD5AQIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlAABg+UHCBDICBisTFWCEiBgsPwAAQIZAYOVqUpQAgQMlh8gQCAjYLAyVQlKgIDB8gMECGQEDFamKkEJEDBYfoAAgYyAwcpUJSgBAgbLDxAgkBEwWJmqBCVAwGD5AQIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlAABg+UHCBDICBisTFWCEiBgsPwAAQIZAYOVqUpQAgQMlh8gQCAjYLAyVQlKgIDB8gMECGQEDFamKkEJEDBYfoAAgYyAwcpUJSgBAgbLDxAgkBEwWJmqBCVAwGD5AQIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlAABg+UHCBDICBisTFWCEiBgsPwAAQIZAYOVqUpQAgQMlh8gQCAjYLAyVQlKgIDB8gMECGQEDFamKkEJEDBYfoAAgYyAwcpUJSgBAgbLDxAgkBEwWJmqBCVAwGD5AQIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlAABg+UHCBDICBisTFWCEiBgsPwAAQIZAYOVqUpQAgQMlh8gQCAjYLAyVQlKgIDB8gMECGQEDFamKkEJEDBYfoAAgYyAwcpUJSgBAgbLDxAgkBEwWJmqBCVAwGD5AQIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlAABg+UHCBDICBisTFWCEiBgsPwAAQIZAYOVqUpQAgQMlh8gQCAjYLAyVQlKgIDB8gMECGQEDFamKkEJEDBYfoAAgYyAwcpUJSgBAgbLDxAgkBEwWJmqBCVAwGD5AQIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlACBB1YxAJfjJb2jAAAAAElFTkSuQmCC"
64 - setIsEditorOpened(false); 17 + ) {
18 + setAlertIsShown(true);
19 + setTimeout(() => {
20 + setAlertIsShown(false);
21 + }, 1000);
22 + } else {
23 + setPreviewURL(lowerCanvas.toDataURL("image/png"));
24 + setIsImgAdded(true);
25 + setIsEditorOpened(false);
26 + }
65 }; 27 };
66 28
67 return ( 29 return (
68 <Container> 30 <Container>
69 - <div onClick={handleEnd} className="upload"> 31 + <div onClick={handleEnd} className="move">
70 - Upload 32 + Move to Gif
71 </div> 33 </div>
72 <ImageEditor 34 <ImageEditor
73 includeUI={{ 35 includeUI={{
74 loadImage: { 36 loadImage: {
75 - // path: 'img/sampleImage.jpg',
76 name: "SampleImage", 37 name: "SampleImage",
77 }, 38 },
78 - // theme: myTheme,
79 - menu: ["shape", "filter"],
80 - initMenu: "filter",
81 uiSize: { 39 uiSize: {
82 width: "100%", 40 width: "100%",
83 - height: "700px", 41 + height: "600px",
84 }, 42 },
85 menuBarPosition: "bottom", 43 menuBarPosition: "bottom",
86 }} 44 }}
...@@ -92,6 +50,9 @@ const ToastEditor = ({ setPreviewURL, setIsImgAdded, setIsEditorOpened }) => { ...@@ -92,6 +50,9 @@ const ToastEditor = ({ setPreviewURL, setIsImgAdded, setIsEditorOpened }) => {
92 }} 50 }}
93 usageStatistics={true} 51 usageStatistics={true}
94 /> 52 />
53 + <div className="alert" style={{ opacity: alertIsShown ? 1 : 0 }}>
54 + Please select a photo.
55 + </div>
95 </Container> 56 </Container>
96 ); 57 );
97 }; 58 };
...@@ -105,7 +66,7 @@ const Container = styled.div` ...@@ -105,7 +66,7 @@ const Container = styled.div`
105 display: flex; 66 display: flex;
106 flex-direction: column; 67 flex-direction: column;
107 align-items: center; 68 align-items: center;
108 - .upload { 69 + .move {
109 font: 800 11.5px Arial; 70 font: 800 11.5px Arial;
110 position: absolute; 71 position: absolute;
111 right: 0; 72 right: 0;
...@@ -122,11 +83,21 @@ const Container = styled.div` ...@@ -122,11 +83,21 @@ const Container = styled.div`
122 justify-content: center; 83 justify-content: center;
123 cursor: pointer; 84 cursor: pointer;
124 } 85 }
86 + .alert {
87 + position: fixed;
88 + border-radius: 0.5rem;
89 + transition: 1s;
90 + top: 7rem;
91 + }
125 .tui-image-editor-container { 92 .tui-image-editor-container {
126 border-radius: 1.5rem; 93 border-radius: 1.5rem;
127 } 94 }
128 .tui-image-editor-container .tui-image-editor-help-menu.top { 95 .tui-image-editor-container .tui-image-editor-help-menu.top {
129 - top: 2rem; 96 + left: 19rem;
97 + top: 1rem;
98 + }
99 + .tui-image-editor-header-logo {
100 + display: none;
130 } 101 }
131 `; 102 `;
132 103
......
1 node_modules 1 node_modules
2 -dist/*
...\ No newline at end of file ...\ No newline at end of file
2 +dist/*
3 +
4 +package-lock.json
5 +yarn.lock
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -59,10 +59,37 @@ ...@@ -59,10 +59,37 @@
59 imageEditor.ui.resizeEditor(); 59 imageEditor.ui.resizeEditor();
60 }; 60 };
61 61
62 + console.log("imageeiasdfasdf", imageEditor);
63 +
62 let gifGenerator; 64 let gifGenerator;
63 setTimeout(function () { 65 setTimeout(function () {
64 gifGenerator = new GifGenerator(imageEditor._graphics.getCanvas()); 66 gifGenerator = new GifGenerator(imageEditor._graphics.getCanvas());
67 + gifGenerator.on("progress", (p) => console.log(p));
65 }, 1000); 68 }, 1000);
69 + function render() {
70 + gifGenerator.make().then(
71 + (blob) => {
72 + window.open(window.URL.createObjectURL(blob));
73 + },
74 + (error) => {
75 + alert(error);
76 + }
77 + );
78 + }
66 </script> 79 </script>
80 + <button
81 + style="
82 + position: absolute;
83 + top: 70px;
84 + right: 70px;
85 + border: 1px solid #fff;
86 + background: rgba(0, 0, 0, 0);
87 + color: #fff;
88 + padding: 10px 20px;
89 + "
90 + onClick="render();"
91 + >
92 + GIF 생성
93 + </button>
67 </body> 94 </body>
68 </html> 95 </html>
......
...@@ -20,7 +20,9 @@ ...@@ -20,7 +20,9 @@
20 }, 20 },
21 "dependencies": { 21 "dependencies": {
22 "@babel/plugin-proposal-class-properties": "^7.13.0", 22 "@babel/plugin-proposal-class-properties": "^7.13.0",
23 + "@dhdbstjr98/gif.js": "^1.0.0",
23 "gifencoder": "^2.0.1", 24 "gifencoder": "^2.0.1",
25 + "hangul-js": "^0.2.6",
24 "stream": "0.0.2" 26 "stream": "0.0.2"
25 } 27 }
26 } 28 }
......
1 +import ComponentInterface from "./ComponentInterface";
2 +import Color from "./Color";
3 +import { fabric } from "fabric";
4 +
5 +class Brush extends ComponentInterface {
6 + constructor(fabricObj) {
7 + super(fabricObj.path.length);
8 + this.color = new Color(fabricObj.stroke);
9 + this.paths = fabricObj.path;
10 + this.size = fabricObj.strokeWidth;
11 + }
12 +
13 + getCurrentFabricObject() {
14 + const paths = this.paths.filter((_, i) => i < this.state.current);
15 + if (paths.length > 0) {
16 + const popCount = paths[paths.length - 1].length - 3;
17 + for (let i = 0; i < popCount; i++) {
18 + paths[paths.length - 1].pop();
19 + }
20 + paths[paths.length - 1][0] = "L";
21 + }
22 + return new fabric.Path(paths, {
23 + stroke: this.color.getRgba(),
24 + strokeWidth: this.size,
25 + fill: null,
26 + strokeLineCap: "round",
27 + strokeLineJoin: "round",
28 + });
29 + }
30 +
31 + next() {
32 + return this.addState(30);
33 + }
34 +}
35 +
36 +export default Brush;
1 +class Color {
2 + constructor(colorData) {
3 + if (colorData[0] == "#") {
4 + this.r = parseInt(colorData.substring(1, 3), 16);
5 + this.g = parseInt(colorData.substring(3, 5), 16);
6 + this.b = parseInt(colorData.substring(5, 7), 16);
7 + this.a = 1;
8 + } else {
9 + const rgba = colorData.substring(5, colorData.length - 1).split(",");
10 + this.r = parseInt(rgba[0]);
11 + this.g = parseInt(rgba[1]);
12 + this.b = parseInt(rgba[2]);
13 + this.a = parseFloat(rgba[3]);
14 + }
15 + }
16 +
17 + getColorCode() {
18 + return `#${this.r.toString(16)}${this.g.toString(16)}${this.b.toString(
19 + 16
20 + )}`;
21 + }
22 +
23 + getRgba() {
24 + return `rgba(${this.r},${this.g},${this.b},${this.a})`;
25 + }
26 +}
27 +
28 +export default Color;
1 +class ComponentInterface {
2 + constructor(maxState) {
3 + this.state = {
4 + current: 0,
5 + max: maxState,
6 + };
7 + }
8 +
9 + getCurrentFabricObject() {}
10 +
11 + addState(count = 1) {
12 + if (this.state.current == this.state.max) {
13 + this.state.current++;
14 + } else {
15 + this.state.current = Math.min(this.state.current + count, this.state.max);
16 + }
17 + }
18 +
19 + end() {
20 + return this.state.current == this.state.max + 1;
21 + }
22 +}
23 +
24 +export default ComponentInterface;
1 +import ComponentInterface from "./ComponentInterface";
2 +import Color from "./Color";
3 +import Hangul from "hangul-js";
4 +import { fabric } from "fabric";
5 +
6 +class Text extends ComponentInterface {
7 + constructor(fabricObj) {
8 + const textSplited = Hangul.disassemble(fabricObj.text);
9 +
10 + super(textSplited.length);
11 + this.color = new Color(fabricObj.fill);
12 + this.text = {
13 + plain: fabricObj.text,
14 + splited: textSplited,
15 + };
16 + this.position = {
17 + top:
18 + fabricObj.originY == "center"
19 + ? fabricObj.top - fabricObj.height / 2
20 + : fabricObj.top,
21 + left:
22 + fabricObj.originX == "center"
23 + ? fabricObj.left - fabricObj.width / 2
24 + : fabricObj.left,
25 + };
26 + this.font = {
27 + size: fabricObj.fontSize,
28 + style: fabricObj.fontStyle,
29 + weight: fabricObj.fontWeight,
30 + family: fabricObj.fontFamily,
31 + };
32 + }
33 +
34 + getCurrentFabricObject() {
35 + return new fabric.Text(
36 + Hangul.assemble(
37 + this.text.splited.filter((_, i) => i < this.state.current)
38 + ),
39 + {
40 + top: this.position.top,
41 + left: this.position.left,
42 + originX: "left",
43 + originY: "top",
44 + fontFamily: this.font.family,
45 + fontSize: this.font.size,
46 + fontWeight: this.font.weight,
47 + fontStyle: this.font.style,
48 + fill: this.color.getColorCode(),
49 + }
50 + );
51 + }
52 +
53 + next() {
54 + return this.addState();
55 + }
56 +}
57 +
58 +export default Text;
1 +import Brush from "./Brush";
2 +import Text from "./Text";
3 +
4 +const Component = {
5 + Brush,
6 + Text,
7 +};
8 +
9 +export default Component;
1 +import GIF from "@dhdbstjr98/gif.js";
2 +import { fabric } from "fabric";
3 +import Component from "./components";
4 +
5 +export class GifGenerator {
6 + constructor(canvas) {
7 + this.canvas = canvas;
8 + this.width = canvas.getWidth();
9 + this.height = canvas.getHeight();
10 + this.events = {};
11 +
12 + this._initializeGif();
13 + }
14 +
15 + _initializeGif() {
16 + this.gif = new GIF({
17 + width: this.width,
18 + height: this.height,
19 + transparent: null,
20 + repeat: 0,
21 + setQuality: 10,
22 + });
23 +
24 + Object.keys(this.events).map((event) => {
25 + this.events[event].map((callback) => {
26 + this.gif.on(event, callback);
27 + });
28 + });
29 + }
30 +
31 + _addFrame(delay = 0) {
32 + this.gif.addFrame(this.canvas.getContext(), { delay, copy: true });
33 + }
34 +
35 + _render(callback) {
36 + this.gif.on("finished", (blob) => {
37 + callback(blob);
38 + });
39 + this.gif.render();
40 + }
41 +
42 + on(event, callback) {
43 + if (!this.events[event]) this.events[event] = [];
44 + this.events[event].push(callback);
45 + }
46 +
47 + make() {
48 + this._initializeGif();
49 +
50 + const fabricObjs = this.canvas.getObjects();
51 + const objs = [];
52 +
53 + fabricObjs.map((fabricObj) => {
54 + if (fabricObj.path !== undefined) {
55 + objs.push(new Component.Brush(fabricObj));
56 + this.canvas.remove(fabricObj);
57 + } else if (fabricObj.text !== undefined) {
58 + objs.push(new Component.Text(fabricObj));
59 + this.canvas.remove(fabricObj);
60 + }
61 + });
62 +
63 + return new Promise((resolve, reject) => {
64 + if (objs.length > 0) {
65 + let objIdx = 0;
66 + let isAddMode = true;
67 + const draw = () => {
68 + const obj = objs[objIdx];
69 + if (isAddMode) {
70 + const fabricObj = obj.getCurrentFabricObject();
71 + obj.next();
72 + isAddMode = false;
73 + this.canvas.add(fabricObj);
74 + } else {
75 + this._addFrame(1);
76 + isAddMode = true;
77 + if (obj.end()) {
78 + if (objIdx < objs.length - 1) {
79 + objIdx++;
80 + draw();
81 + } else {
82 + this.canvas.off("after:render", draw);
83 + this._render(resolve);
84 + }
85 + } else {
86 + this.canvas.remove(
87 + this.canvas._objects[this.canvas._objects.length - 1]
88 + );
89 + }
90 + }
91 + };
92 + this.canvas.on("after:render", draw);
93 + draw();
94 + } else {
95 + reject(new Error("no objects"));
96 + }
97 + });
98 + }
99 +}
100 +
101 +window.GifGenerator = GifGenerator;
1 +import { useRouter } from "next/dist/client/router";
2 +
3 +const Detail = () => {
4 + const id = useRouter().query.id;
5 +
6 + return (
7 + <div>
8 + <img
9 + src={`https://9davbjzey4.execute-api.ap-northeast-2.amazonaws.com/?id=${id}`}
10 + />
11 + </div>
12 + );
13 +};
14 +
15 +export default Detail;
1 import Header from "components/Header"; 1 import Header from "components/Header";
2 -import Image from "components/Image";
3 import styled from "styled-components"; 2 import styled from "styled-components";
4 import dynamic from "next/dynamic"; 3 import dynamic from "next/dynamic";
5 import { useState } from "react"; 4 import { useState } from "react";
...@@ -7,6 +6,9 @@ import { useState } from "react"; ...@@ -7,6 +6,9 @@ import { useState } from "react";
7 const ToastEditor = dynamic(() => import("components/ToastEditor"), { 6 const ToastEditor = dynamic(() => import("components/ToastEditor"), {
8 ssr: false, 7 ssr: false,
9 }); 8 });
9 +const GifEditor = dynamic(() => import("components/GifEditor"), {
10 + ssr: false,
11 +});
10 12
11 const Index = () => { 13 const Index = () => {
12 const [isEditorOpened, setIsEditorOpened] = useState(false); 14 const [isEditorOpened, setIsEditorOpened] = useState(false);
...@@ -35,19 +37,7 @@ const Index = () => { ...@@ -35,19 +37,7 @@ const Index = () => {
35 </button> 37 </button>
36 </div> 38 </div>
37 ) : ( 39 ) : (
38 - !isEditorOpened && ( 40 + !isEditorOpened && <GifEditor {...{ previewURL }} />
39 - <>
40 - <div style={{ position: "fixed", top: "5rem" }}>
41 - <button
42 - className="open-button"
43 - onClick={() => setIsEditorOpened(true)}
44 - >
45 - Change Image
46 - </button>
47 - </div>
48 - <Image {...{ previewURL, setPreviewURL }} />
49 - </>
50 - )
51 )} 41 )}
52 {isEditorOpened && ( 42 {isEditorOpened && (
53 <ToastEditor {...{ setPreviewURL, setIsImgAdded, setIsEditorOpened }} /> 43 <ToastEditor {...{ setPreviewURL, setIsImgAdded, setIsEditorOpened }} />
......
...@@ -1367,6 +1367,11 @@ ...@@ -1367,6 +1367,11 @@
1367 resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.1.tgz#78b5433344e2f92e8b306c06a5622c50c245bf6b" 1367 resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.1.tgz#78b5433344e2f92e8b306c06a5622c50c245bf6b"
1368 integrity sha512-S6oPal772qJZHoRZLFc/XoZW2gFvwXusYUmXPXkgxJLuEk2vOt7jc4Yo6z/vtI0EBkbPBVrJJ0B+prLIKiWqHg== 1368 integrity sha512-S6oPal772qJZHoRZLFc/XoZW2gFvwXusYUmXPXkgxJLuEk2vOt7jc4Yo6z/vtI0EBkbPBVrJJ0B+prLIKiWqHg==
1369 1369
1370 +"@types/fabric@^4.2.5":
1371 + version "4.2.5"
1372 + resolved "https://registry.yarnpkg.com/@types/fabric/-/fabric-4.2.5.tgz#095e412b49c896e2fa0d441bd2fceb6690074c4d"
1373 + integrity sha512-nifrrxvgsYRoxNJB+xZUBe6pLWoqGbZdfwJ0y9zzdt3uU89SzP+8L9Whwrxbvu7eIXjPArSZOyuQmbD4zewdZA==
1374 +
1370 "@types/hoist-non-react-statics@*": 1375 "@types/hoist-non-react-statics@*":
1371 version "3.3.1" 1376 version "3.3.1"
1372 resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" 1377 resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
...@@ -1851,6 +1856,13 @@ axe-core@^4.0.2: ...@@ -1851,6 +1856,13 @@ axe-core@^4.0.2:
1851 resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.1.2.tgz#7cf783331320098bfbef620df3b3c770147bc224" 1856 resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.1.2.tgz#7cf783331320098bfbef620df3b3c770147bc224"
1852 integrity sha512-V+Nq70NxKhYt89ArVcaNL9FDryB3vQOd+BFXZIfO3RP6rwtj+2yqqqdHEkacutglPaZLkJeuXKCjCJDMGPtPqg== 1857 integrity sha512-V+Nq70NxKhYt89ArVcaNL9FDryB3vQOd+BFXZIfO3RP6rwtj+2yqqqdHEkacutglPaZLkJeuXKCjCJDMGPtPqg==
1853 1858
1859 +axios@^0.21.1:
1860 + version "0.21.1"
1861 + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
1862 + integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
1863 + dependencies:
1864 + follow-redirects "^1.10.0"
1865 +
1854 axobject-query@^2.2.0: 1866 axobject-query@^2.2.0:
1855 version "2.2.0" 1867 version "2.2.0"
1856 resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" 1868 resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
...@@ -3086,7 +3098,7 @@ extsprintf@^1.2.0: ...@@ -3086,7 +3098,7 @@ extsprintf@^1.2.0:
3086 resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" 3098 resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
3087 integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= 3099 integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
3088 3100
3089 -fabric@^4.2.0: 3101 +fabric@^4.2.0, fabric@^4.4.0:
3090 version "4.4.0" 3102 version "4.4.0"
3091 resolved "https://registry.yarnpkg.com/fabric/-/fabric-4.4.0.tgz#2b73454008b8082f2d234c4637bdf645876c490c" 3103 resolved "https://registry.yarnpkg.com/fabric/-/fabric-4.4.0.tgz#2b73454008b8082f2d234c4637bdf645876c490c"
3092 integrity sha512-mX6BZqssJjrT6LN1B4Wcmgm93NIlmKfPN5qTqon9wdDJgRAxPfrhfz2iT+QmDso9P8+s0qyLXFhuVpxOBBMHEw== 3104 integrity sha512-mX6BZqssJjrT6LN1B4Wcmgm93NIlmKfPN5qTqon9wdDJgRAxPfrhfz2iT+QmDso9P8+s0qyLXFhuVpxOBBMHEw==
...@@ -3179,6 +3191,11 @@ flatted@^3.1.0: ...@@ -3179,6 +3191,11 @@ flatted@^3.1.0:
3179 resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" 3191 resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469"
3180 integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== 3192 integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==
3181 3193
3194 +follow-redirects@^1.10.0:
3195 + version "1.14.1"
3196 + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43"
3197 + integrity sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==
3198 +
3182 foreach@^2.0.5: 3199 foreach@^2.0.5:
3183 version "2.0.5" 3200 version "2.0.5"
3184 resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" 3201 resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
......