글 내용 정리
1. 소개
2. 실패 리스트
3. gif 변환 방법 소개
4. 작동결과
1. 소개
결론부터 말하면 일반적인 방법으로는 저장할 수 없다. ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
대신에 대충 길고 길었던 삽질 끝에 편법을 찾아내서 글을 작성한다... (2023.2.1 ~ 2023.3.26)
일단 한 달 내내 보고있는데 계속 같은 페이지만 맴도는 것 같아서 실패했던 사례들을 정리해둬야겠다.
진짜 너무 화나고 오기생기네 이거 ㅋㅋ
먼저 하려는건 이런식으로 우리가 html에서 열심히 만들어놓은 화면을 gif file로 내보내는 작업이다.
지금은 사라졌지만... 던전앤파이터 게임의 캐릭터 프로필 카드를 만들어주는 사이트가 있었는데, 간단히 말하면 홈페이지에서 캐릭터 정보를 조회해서 캐릭터카드를 보여주고 사진과 같이 다운로드 할 수 있는 사이트였다.
그리고 이런식으로 서비스가 운영이 됐었으니까 당연히 금방 만들 수 있을 줄 알았는데... 이걸 막상 내가 만들어볼려니까 방법을 모르겠는거임... 한 달 내내 퇴근하면 이거만 고민중인데 진짜 미치겠네 ㅋㅋㅋㅋㅋㅋㅋ
애초에 이걸 프론트단에서 gif을 만드는건지 아니면 백단으로 넘겨서 만들어야 하는건지부터가 애매하단말이지.
아, 프로젝트는 스프링으로 개발중이고, 글에서 말하는 백단은 java 프론트단은 vanilla javascript라고 보면 된다.
2. 실패 리스트
2.1) 첫 번째 시도 (~ 2023. 3. 1)
먼저 처리할 수 있을법한 방법들을 생각해봤다.
1. 적절한 라이브러리가 있어서 쉽게 gif file로 변환할 수 있는가?
음... 뭔가 gif을 가지고 비스무리한 걸 하는 것들이 있는 것 같긴한데...
더 찾아봐야겠지만 딱 확실하게 이거다 하는건 없는듯ㅠ
2. gif 이미지 파일을 핸들링 할 수 있는 라이브러리가 있는가?
내가 영어 까막눈이라 하루종일 번역기 돌리면서 블로그랑 스오플 탐방해봤는데 아닌 것 같다.
아니 진짜 코딩 잠깐 내려두고 영어를 공부해야하나 싶다.
3. 화면 자체를 프레임단위로 캡쳐하고 캡쳐한 프레임들을 묶어서 gif file로 내보낼 수 있는가?
일단 이 방법도 아닌 듯 하다. 애초에 키보드의 프린트스크린 key code가 없어서 key event를 못 주니까 다른 방식으로 캡쳐해야한다. 그리고 그런 방식 중 하나가 바로 그 유명한 html2canvas 라이브러리를 사용한 방법이다.
html/javascript convert to gif 이런식으로 검색해보면 html2canvas에 대한 글들이 많은데, 작동방식을 간단히 말하자면 html에서 canvas로 작성되었거나 div로 작성된걸 canvas로 변환 후 이미지 파일로 저장하는 식이다.
근데 이거도 결국 못 써먹는데... 이유는 다음과 같다.
진짜 웃긴게 이 방식은 화면을 캡쳐하는 게 아니라 canvas로 변환할 때, 사용된 gif의 "첫 번째 프레임"만 canvas에 그린다. 자세하게는 모르겠지만 html2canvas를 사용해서 div를 canvas로 변환할 때, 화면 자체를 캡쳐하는 게 아니라 렌더링이 된 화면을 변환하기 때문이라고 그랬던가...? 결론은 프레임 단위로 변환하는게 아니라 첫 프레임만 가져오는거니까 뭐... 그냥 스크린 샷이라는 말이지.
"프레임 단위로 캡쳐를 뜬 다음에 그 프레임들을 모아서 gif file로 던져주면 되지 않을까?" 그런 희망을 품었지만........
캡쳐를 한다고 해도 화면 자체를 프린트스크린해서 캡쳐하는게 아니라(위에서 말했듯이 printscreen keycode가 없음) html element의 정보를 가져와서 그 정보를 이미지로 저장하는 식.. 인것 같다. 그리고 이게 방금 말한 html2canvas의 방식이라고 생각하면 된다.
아무튼 그래서 화면을 프레임 단위로 변환할 수 있는가... 하고 찾아봤는데 그런 기능은 없는듯?
반복문으로 프레임 한 장씩 캔버스를 변환해서 노가다 할 수도 있겠지... 라고 생각했는데 위에서 말했듯이 그 핸들링 할 수 있는 방법이 없는 것 같더라고... 사실 어딘가 방법은 있는데 내가 못 찾는거겠지ㅠㅠ
아니 그럼 html element를 gif으로 변환하는 방법이 있기는 한건가?
html element convert to gif 이런식으로 구글 선생님께 질문드리면 대충 gif 관련해서 스오플 선생님들에게 드리는 질문글이 몇 개 있고 네이버 블로그도 있고 하던데...
대충 취합하면 이정도 된다.
1) java imageio를 이용해서 gif으로 변환하기
2) html2canvas를 사용해라
3) MediaRecorder API를 사용해라
1) ImageIO
이거에 대한 정보는 네이버 블로그에서 많이 등장한다.
네이버 블로그에는 고수 선생님들이 여기저기 은둔하고 계신다고 해서 조금은 기대를 했었는데... 기대를 배신하셨다.
애초에 그 코드들을 차근차근 바라보고 있으면 진짜 개소리인게 .gif 확장자로 저장이 된다 뿐이지 해당 파일에는 프레임 한 장 밖에 안들어간다. ㅋㅋㅋㅋㅋㅋㅋㅋ 그니까 뭐 그냥 프레임 한 장 짜리 .gif 파일이라는 말이다.
아무튼 ImageIO를 사용해서 gif file로 만들려면 굉장히 수고스러운 코드를 짜야하는 것 같다.
먼저 프론트에서 백단으로 image(png, gif)를 넘겨서 프론트에서 설정했던 위치와 동일하게 설정해서 합쳐야하는데,
그 와중에 gif은 프레임 단위로 받아와야 하고 뭐 이것저것 해줘야 하는게 많은 것 같다...
물론 직접 안해봐서 될 지 안 될지 모르겠는데 이거 할바에 그냥 gifcam 링크 달아두고 캡쳐뜨라고 하는 게 낫지 않을까?
다른 방법이 안되면 이렇게라도 해야겠지만 하고 싶지는... 않네... 최후의 보루로 남겨둔다.
2. html2canvas
위에서 말했지만 안된다.
3) MediaRecorder
얘는 대충 보니까 window영역이나 video객체를 가지고 이래저래 가지고 노는 것 같다.
이걸 해볼려고 했는데 휴일이 다 가서 이번 주말에 트라이해볼려고 한다...
html2canvas를 기반으로.. 그니까 결국 canvas를 화면으로 변환하는 기능인데 일단 결론부터 말하면 되기는 한다.
근데 원본이 심하게 깨진다는 단점이 있다... ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
2.2) 두 번째 시도 (~ 2023. 3. 5)
계속 구글링을 하고 있었는데 "screen capture api"라고 화면을 공유해 줄 수 있는 신기한 기능을 찾았다.
근데 일단 결론부터 말하면 이것도 안된다. 하루종일 한 것 치곤 시간 날렸네 싶긴했는데 재미있었으니까 괜찮아...ㅋㅋㅋㅋㅋㅋㅋ
이어서 작성하자니 글이 너무 길어져서 새로 작성했다. 궁금하면 아래 링크 참고하자.
https://1545154.tistory.com/100
[Screen Capture API] 화면 캡쳐를 해보자
글 내용 정리 1. screen capture api 소개 2. navigator.mediaDevices undefined 문제 3. 로컬에서 ssl 인증서 설치하기 지금 진행중인 프로젝트에서 필요한 기능을 찾다가 재밌는 기능이 있어서 한 번 사용해봤다.
1545154.tistory.com
일단 보다시피 mp4로 다운로드 후 gif으로 변환해서 사용할 수 있다.
있긴한데... 솔직히 구현도 쉽고 속도도 빠르고 해상도도 깔끔하게 캡쳐되지만 방법이 방법인지라 패스하기로 했다.
3. gif 변환 방법 소개
이 작업을 서버에서 처리할지 클라이언트에서 처리할지 많은 고민을 했었지만 필요없는 고민이었다.
결론부터 말하면 node.js 라이브러리가 있어서 서버로 데이터를 넘겨서 작업 후 프론트로 넘겨주는 두 번일을 할 필요가 없었기 때문에 클라이언트에서 처리하는 방식을 사용했다.
서버에서 작업을 하면 결과물이 더 좋아지는지 여부는 모르겠지만 내가 보기엔 그닥 크게 차이나지는 않을 것 같다. 복잡하기도 하고.
근데 내 방법이 일반적이지는 않다는 점을 먼저 말해둔다. (어차피 내 입장에서는 결과물만 나오면 되니까...)
위에서 말했던 "프레임 단위로 캡쳐를 뜬 다음에 그 프레임들을 모아서 gif file로 던져주면 되지 않을까?" 방법이다.
사용한 라이브러리는 merge-images와 gifshot.js다.
https://www.npmjs.com/package/merge-images (v2를 써야 image resize 할 수 있음)
https://www.npmjs.com/package/merge-images-v2
merge-images-v2
Easily compose images together without messing around with canvas, but has canvas for node. Latest version: 2.0.1, last published: 4 years ago. Start using merge-images-v2 in your project by running `npm i merge-images-v2`. There are no other projects in t
www.npmjs.com
https://www.npmjs.com/package/gifshot
gifshot
JavaScript library that can create animated gifs from video streams (e.g. webcam), existing videos (e.g. mp4), or existing images. Latest version: 0.4.5, last published: 5 years ago. Start using gifshot in your project by running `npm i gifshot`. There are
www.npmjs.com
node 사용안할거면 파일을 다운로드 받아서 스크립트를 추가해주면 된다.
<script src="layout/assets/js/mergeImagesV2.js"></script>
<script type="module" src="layout/assets/js/gifshot.js"></script>
gifshot은 그대로 쓰면 되는데 mergeImageV2는 수정이 필요하다.
merge-images-v2/src/index.js 다운로드 받아서 가장 하단에 export modules를 지워주자.
내가 설정을 대충해서 그런지 모듈 인식이 안되더라고...
스크립트 적용 후 개발자도구에서 이런식으로 나오면 오케이다.
프로세스를 먼저 설명하겠다.
이런 느낌의 프로필을 다운로드 할 수 있는 기능을 만들려고 한다.
기능을 구현하기 위해 크게 세 가지 영역으로 나눴다.
목적은 Base위에 png와 gif을 올려서 하나의 gif을 만드는것이다.
Base 위에 png를 올리는건 한 번의 작업으로 끝나지만 gif을 올리려면 gif의 프레임 개수만큼의 작업이 필요하다.
그러니까 gif의 프레임이 3장이라면 Base+png+gif(frame:1), Base+png+gif(frame:2), Base+png+gif(frame:3)이 하나의 gif이 된다.
먼저 내가 가지고 있는 gif들을 프레임단위로 쪼갰다.
이것도 변환할 수 있는 라이브러리가 있었는데 이것까지 처리해줄려면 진짜 오래걸린다...
애초에 그럴경우 가지고 있는 gif들의 프레임 개수나 프레임 속도가 똑같을리가 없으므로 추가로 정규화 처리를 해줘야해서 굉장히 복잡하다.
어차피 한번만 해두면 되니까 직접 수작업으로 변환시키기로 했다.
프레임 단위로 변환시키는 작업 자체는 그냥 노가다라서 적당히 나눠주면 된다.
폴더를 나누고 각 gif들에 대해 같은 방법으로 정규화시켰다.
단순하게 mergeImages( [ image ] )로 하나의 프레임을 png로 생성 후 gifShot( [ image ] )로 생성된 이미지들을 gif으로 변환하는게 전부다.
50번을 반복한 이유는 내가 정규화한 gif의 프레임 개수가 50개라서 총 50장의 png를 만들기 위함이다
그 외엔 딱히 코드 리뷰는 할 게 없으니 코드 전체와 작동결과에 대해서만 리뷰하겠다.
// gif 만들기
let img = [];
function mergePng(){
return new Promise(function(resolve, reject){
let cnt = 1;
for (let index=1; index<50; index++){
mergeImages([
{ src : 'layout/images/pic5.png', x:0, y:0 ,opacity:0.1 },
{ src : 'layout/images/pic6.png', x:300, y:200 ,opacity:1 },
{ src : 'layout/images/gif/gifdownload/p1/' + index +'.png', x:50, y:50 ,opacity:1},
{ src : 'layout/images/gif/gifdownload/p1/' + index +'.png', x:50, y:100 ,opacity:1},
{ src : 'layout/images/gif/gifdownload/p1/' + index +'.png', x:50, y:150 ,opacity:1},
{ src : 'layout/images/gif/gifdownload/p1/' + index +'.png', x:50, y:200 ,opacity:1},
{ src : 'layout/images/gif/gifdownload/p1/' + index +'.png', x:50, y:250 ,opacity:1},
{ src : 'layout/images/gif/gifdownload/p2/' + index +'.png', x:100, y:100 ,opacity:1},
{ src : 'layout/images/gif/gifdownload/p2/' + index +'.png', x:100, y:150 ,opacity:1},
{ src : 'layout/images/gif/gifdownload/p2/' + index +'.png', x:100, y:200 ,opacity:1},
{ src : 'layout/images/gif/gifdownload/p2/' + index +'.png', x:100, y:250 ,opacity:1},
{ src : 'layout/images/gif/gifdownload/p3/' + index +'.png', x:150, y:50 ,opacity :1},
{ src : 'layout/images/gif/gifdownload/p3/' + index +'.png', x:150, y:100 ,opacity:1},
{ src : 'layout/images/gif/gifdownload/p3/' + index +'.png', x:150, y:150 ,opacity:1},
{ src : 'layout/images/gif/gifdownload/p2/' + index +'.png', x:450, y:150 ,opacity:1},
{ src : 'layout/images/gif/gifdownload/p2/' + index +'.png', x:450, y:200 ,opacity:1},
{ src : 'layout/images/gif/gifdownload/p2/' + index +'.png', x:450, y:250 ,opacity:1},
{ src : 'layout/images/gif/gifdownload/p3/' + index +'.png', x:500, y:100 ,opacity:1},
{ src : 'layout/images/gif/gifdownload/p3/' + index +'.png', x:500, y:150 ,opacity:1},
{ src : 'layout/images/gif/gifdownload/p3/' + index +'.png', x:500, y:200 ,opacity:1},
{ src : 'layout/images/gif/gifdownload/p4/' + index +'.png', x:550, y:50 ,opacity: 1},
{ src : 'layout/images/gif/gifdownload/p4/' + index +'.png', x:550, y:100 ,opacity:1},
{ src : 'layout/images/gif/gifdownload/p4/' + index +'.png', x:550, y:150 ,opacity:1},
{ src : 'layout/images/gif/gifdownload/p4/' + index +'.png', x:550, y:200 ,opacity:1},
{ src : 'layout/images/gif/gifdownload/p4/' + index +'.png', x:550, y:250 ,opacity:1},
{ src : 'layout/images/gif/gifdownload/p4/' + index +'.png', x:550, y:300 ,opacity:1},
{ src : 'layout/images/gif/gifdownload/ep1/' + index +'.png', x:50, y:50 ,opacity:0.7},
{ src : 'layout/images/gif/gifdownload/ep2/' + index +'.png', x:50, y:100 ,opacity:0.7},
{ src : 'layout/images/gif/gifdownload/ep3/' + index +'.png', x:50, y:150 ,opacity:0.7},
{ src : 'layout/images/overlay.png', x:50, y:200 ,opacity:0.4},
{ src : 'layout/images/overlay.png', x:50, y:250 ,opacity:0.4},
{ src : 'layout/images/gif/gifdownload/ep1/' + index +'.png', x:100, y:100 ,opacity:0.6},
{ src : 'layout/images/gif/gifdownload/ep2/' + index +'.png', x:100, y:150 ,opacity:0.6},
{ src : 'layout/images/gif/gifdownload/ep3/' + index +'.png', x:100, y:200 ,opacity:0.6},
{ src : 'layout/images/overlay.png', x:100, y:250 ,opacity:0.4},
{ src : 'layout/images/gif/gifdownload/ep1/' + index +'.png', x:150, y:50 ,opacity :0.5},
{ src : 'layout/images/gif/gifdownload/ep2/' + index +'.png', x:150, y:100 ,opacity:0.5},
{ src : 'layout/images/gif/gifdownload/ep3/' + index +'.png', x:150, y:150 ,opacity:0.5},
{ src : 'layout/images/gif/gifdownload/ep1/' + index +'.png', x:450, y:150 ,opacity:0.4},
{ src : 'layout/images/gif/gifdownload/ep2/' + index +'.png', x:450, y:200 ,opacity:0.4},
{ src : 'layout/images/gif/gifdownload/ep3/' + index +'.png', x:450, y:250 ,opacity:0.4},
{ src : 'layout/images/gif/gifdownload/ep1/' + index +'.png', x:500, y:100 ,opacity:0.3},
{ src : 'layout/images/gif/gifdownload/ep2/' + index +'.png', x:500, y:150 ,opacity:0.3},
{ src : 'layout/images/gif/gifdownload/ep3/' + index +'.png', x:500, y:200 ,opacity:0.3},
{ src : 'layout/images/gif/gifdownload/ep1/' + index +'.png', x:550, y:50 ,opacity :0.2},
{ src : 'layout/images/gif/gifdownload/ep2/' + index +'.png', x:550, y:100 ,opacity:0.2},
{ src : 'layout/images/gif/gifdownload/ep3/' + index +'.png', x:550, y:150 ,opacity:0.2},
{ src : 'layout/images/overlay.png', x:550, y:200 ,opacity:0.4},
{ src : 'layout/images/overlay.png', x:550, y:250 ,opacity:0.4},
{ src : 'layout/images/overlay.png', x:550, y:300 ,opacity:0.4},
{ src : 'layout/images/pic2.png', x:50, y:50 },
{ src : 'layout/images/pic2.png', x:50, y:100 },
{ src : 'layout/images/pic2.png', x:50, y:150 },
{ src : 'layout/images/pic2.png', x:50, y:200 },
{ src : 'layout/images/pic2.png', x:50, y:250 },
{ src : 'layout/images/pic1.png', x:100, y:100},
{ src : 'layout/images/pic1.png', x:100, y:150},
{ src : 'layout/images/pic1.png', x:100, y:200},
{ src : 'layout/images/pic1.png', x:100, y:250},
{ src : 'layout/images/pic2.png', x:150, y:50 },
{ src : 'layout/images/pic2.png', x:150, y:100},
{ src : 'layout/images/pic2.png', x:150, y:150},
{ src : 'layout/images/pic2.png', x:450, y:150},
{ src : 'layout/images/pic2.png', x:450, y:200},
{ src : 'layout/images/pic2.png', x:450, y:250},
{ src : 'layout/images/pic1.png', x:500, y:100},
{ src : 'layout/images/pic1.png', x:500, y:150},
{ src : 'layout/images/pic1.png', x:500, y:200},
{ src : 'layout/images/pic2.png', x:550, y:50 },
{ src : 'layout/images/pic2.png', x:550, y:100},
{ src : 'layout/images/pic2.png', x:550, y:150},
{ src : 'layout/images/pic2.png', x:550, y:200},
{ src : 'layout/images/pic2.png', x:550, y:250},
{ src : 'layout/images/pic2.png', x:550, y:300}
]
)
.then( function(res){
console.log("merge OK", index, cnt);
cnt++;
img.push(res);
if(cnt == 50){
createAllGif();
}
});
}
resolve(img);
})
}
async function createAllGif(){
console.log("ss");
console.log(img);
gifshot.createGIF({
'images': img,
gifWidth: 1102,
gifHeight: 720,
},function(obj) {
console.log("obj:",obj);
if(!obj.error) {
var image = obj.image,
animatedImage = document.createElement('img');
animatedImage.src = image;
document.body.appendChild(animatedImage);
$(".testImg").append(animatedImage);
downloadButton.href = image;
downloadButton.download = "RecordedVideo.gif";
downloadButton.click();
console.log("Aaaaaaa");
}
});
}
4. 작동방법
개발 직후 바로 작성하는거라 화면이 조금 더럽지만... 기능만 확인하면 되니까 넘어가자.
MERGEPNG 버튼을 클릭하면 위에서 작성했던 코드가 실행된다.
GIF 크기가 클수록 변환시간이 굉장히 길어지는데, 적당히 크기를 줄이기를 권한다.
내 프로그램에서는 3초내지로 완료되는 것 같다.
작업이 끝나면 파일이 다운로드 되므로 확인해보면 된다.
참고로 확인용으로 생성된 GIF을 하단에 출력하도록 테스트코드를 남겨놨는데 실제로 서비스할때는 지우면 된다.
생성된파일은 다음과 같다.
뭐 일단 테스트용으로 적당히 만들어봤는데 화질이 영 별로라서 이걸 어떻게 좀 개선할 수 없을지 고민이다.
이전에 테스트했었던 Screen Capture Api를 사용할 수 있다면 이런 고민은 하지 않아도 될텐데...
고민을 더 해본 후 이 프로젝트는 적당히 완료해야겠다.
이 프로젝트로만 꼬박 반 년을 태운 것 같다.
프론트쪽은 처음이라서 찾아보는 데 시간이 많이 소요되었던 것 같다. ㅋㅋㅋ
그래도 이제 한 달 이내로는 검수까지 끝낼 수 있을 것 같아서 후련한 기분이다.
끝!
'공부 > html & javascript' 카테고리의 다른 글
[Screen Capture API] javascript로 화면을 동영상으로 캡쳐 후 저장까지 (0) | 2023.03.06 |
---|
댓글