Airtable 및 Firebase를 사용하여 사용자가 제출한 이미지 수집 및 처리

빠른 주말 계획Loungeware는 지역사회에서 개발한 Warioware 스타일의 게임으로 지역사회에서 예술, 코드와 음악을 제공한다.이 게임의 특징은 사용자가 라로드라는 캐릭터의 이미지를 제출한 것이다.



스페이스
#우주 베이킹

Loungeware, community Studio2로 제작된 Warioware 스타일의 협업 게임 소개
2021년 8월 4일 오전 00:12
이전에 Larolds는 로 제출되었습니다.Discord를 통해 전송된 png 파일은 여러 단계로 처리해야 합니다.
  • 200x200px
  • 이미지 확인
  • 이미지를 투톤 팔레트에 고정시킵니다
  • .
  • 참여자 이름과 기타 메타데이터를 코드
  • 의 수조에 수집
  • 이미지를 정령의 프레임에 복사하여 정령의 이미지 인덱스와 메타데이터 그룹이 일치하도록 확보
  • 이미지와 메타데이터를 각각 온라인 갤러리/학점
  • 의 사이트 저장소에 복사
    이 과정은 비록 간단하지만 시간이 소모되고 오류가 발생하기 쉽기 때문에 나는 자동화를 실현하고 싶다.이를 위해, 나는 Airtable을 사용할 것이다. 이것은 내가 웹 기반 폼을 만들어서 사용자가 이미지와 기타 데이터를 제출할 수 있도록 할 것이다.처리된 이미지를 처리하고 저장하는 데 사용되는 Firebase 함수입니다.

    비행의가 있다


    Airtable은 전자 표와 데이터베이스를 결합한 온라인 서비스이다.API 질의를 사용할 수 있는 데이터베이스를 만들 수 있습니다.제출 폼도 만들 수 있습니다. 이것이 바로 우리가 여기서 해야 할 일입니다.
    나는 Larold 제출을 위해 간단한 데이터베이스를 만들었는데, 이것은 데이터의 격자 보기 (즉 전자 표 보기) 이고, 내가 설정한 열을 보여 준다.

    설정이 완료되면 사용자가 데이터베이스에 데이터를 제출할 수 있도록 새로운 공공 폼을 만들 수 있습니다.데이터와 격자 보기는 개인적이지만, 사용자는 공공 폼을 사용하여 새로운 제출 내용을 발표할 수 있다.구글 문서에 익숙한 사람들은 구글 폼과 매우 비슷하다는 것을 발견할 수 있다

    관리자만 볼 수 있는 괜찮은 보기는 갤러리 보기입니다. 그림의 더 큰 보기를 보여 줍니다.

    API Airtable 액세스


    데이터에 대한 프로그래밍 접근이 없으면 자동화를 실현할 수 없다.Airtable을 선택한 이유는 데이터에 접근하기 위해 사용하기 쉬운 API이기 때문입니다.
    우선, 우리는 나의 계정 설정을 통해 API 키를 생성해야 한다

    다음에 Postman을 사용하여 HTTP 요청을 통해 데이터를 가져올 수 있습니다!

    위의 화면 캡처를 통해 알 수 있듯이 데이터베이스에 있는 기록은 JSON 구조의 형식으로 기록 수조에 나타나고 완전한 필드 이름은 키로 한다.업로드된 이미지는 Airtable CDN의 공용 URL을 통해 제공됩니다.

    이미지 처리


    일부 이미지의 크기나 색상이 정확하지 않기 때문에, 우리는 이 이미지들을 처리할 것입니다.명령행 이미지 처리 도구인 Imagemagick을 오랫동안 사용했습니다.다행히도 Firebase 함수 execution environment 는 Imagemagick을 실제로 설치했습니다. 이것은 우리가 그것을 사용하여 그림을 처리할 수 있다는 것을 의미합니다. (사실상 이 환경은 ffmpeg도 포함됩니다.)Firebase 함수를 사용하면 트리거될 때 다음 작업을 수행합니다.
  • Airtable
  • 에서 최신 데이터 가져오기
  • 갤러리 사이트
  • 에서 메타데이터를 사용할 수 있도록 데이터를Firestore에 동기화
  • 필요에 따라 이미지를 처리한 후 이를 클라우드 저장소에 저장하여 갤러리
  • 에서 이 데이터를 사용할 수 있도록 한다.
  • PNG 이미지
  • 에 모든 Larold 이미지를 포함하는 엘프 스틱 생성
  • 엘프 게이지와 메타데이터를 json 형식으로 되돌려줍니다.zip 파일
  • 1단계: Airtable에서 최신 데이터 가져오기


    일을 더욱 간단하게 하기 위해서 나는 정부Airtable npm package를 이용하여 API를 방문한다.언제 시작 해요?
    Airtable 패키지를 사용하면 액세스 권한을 쉽게 설정할 수 있습니다.
    const functions = require("firebase-functions");
    const Airtable = require("airtable");
    
    Airtable.configure({
      endpointUrl: "https://api.airtable.com",
      apiKey: functions.config().airtable.api_key,
    });
    const base = Airtable.base(functions.config().airtable.base);
    
    async function doSync() {
      const records = await base("Larolds").select({
        view: "Grid view",
      }).all();
    }
    
    코드에서 하드코딩 민감한 값을 피하기 위해 Firebasefunctions.config()를 사용하여 환경에서 기밀을 얻습니다.설정된 후 base("Larolds").select().all(); 모든 기록을 가져옵니다. (페이지를 처리합니다.)결과는 교체할 수 있는 기록 구조다.

    2단계: Firestore와 동기화


    Firestore 설정을 건너뛸 것입니다. (다른 안내서도 있습니다.)모든 기록을 동기화하고 있기 때문에, 불행하게도,Firestore 집합에서 모든 기록을 꺼내서 수정 날짜를 확인하고 변경 사항을 작성해야 합니다.파이어스토어는 항상 한 번에 모든 기록을 갱신하는 상황에 적합하지 않기 때문에 어색하다.실제로, 나는 접근 비용을 최적화하기 위해 이 모든 데이터를 Firestore 문서에 써야 한다.그러나 트래픽이 적은 사이트의 경우 개인 문서를 제공하고 필요한 경우 업데이트합니다.
    const records = await base("Larolds").select({
        view: "Grid view",
      }).all();
    
      functions.logger.info("Got larolds from airtable", {count: records.length});
    
      const existingDocuments = await laroldStore.listDocuments();
      const existingData = Object.fromEntries(existingDocuments.map((doc) => [doc.id, doc.data]));
    
      // Update image
      const laroldData = await Promise.all(records
          .filter((record) => (record.get("Image file").length > 0 && record.get("Confirmed for use") == "Yes"))
          .map(async (record, idx) => {
            const image = record.get("Image file")[0];
            const id = image.id; // use the image unique ID as id
            const modified = record.get("Last modified");
    
            // Check if updated
            let doc;
            if (!existingData[id] || existingData[id].modified != modified) {
              const imageUrl = image.url;
              const {warnings, destination} = await processImage(imageUrl, image.filename, id);
              doc = {
                id: id,
                name: record.get("Larold name"),
                attribution: record.get("Attribution name"),
                submitter: record.get("Submitter"),
                imageUrl,
                modified,
                idx: idx+1,
                warnings,
                destination,
              };
              await laroldStore.doc(id).set(doc);
            } else {
              doc = existingData[id];
            }
    
            return doc;
          }));
      const updatedIds = laroldData.map((doc) => doc.id);
      functions.logger.info("Updated larolds in store", {updatedIds});
    
      // Remove old ones
      const deleteDocs = existingDocuments.filter((doc) => !updatedIds.includes(doc.id));
      const deletedIds = deleteDocs.map((doc) => doc.id);
      await Promise.all(deleteDocs.map((doc) => doc.delete()));
    
    이 큰 스크립트는 Airtable와Firestore에서 모든 기록을 가져와서 그것들을 교체해서 어떤 문서가 업데이트되어야 하는지, 어떤 문서가 유행이 지났는지, 그리고 그것을 삭제해야 하는지, 그리고 데이터를 zip에서 되돌아올 대상으로 되돌려줍니다.
    위 코드에 한 줄const {warnings, destination} = await processImage(imageUrl, image.filename, id);이 있으니 다음에 소개해 드리겠습니다.이 코드가 이 if 검사에 있는 이유는 처리된 이미지를 처리해야 하는 것을 피하기 위해서입니다.
    Firebase의 우수한 로컬 시뮬레이터를 통해 결과를 볼 수 있습니다. 이 시뮬레이터는 로컬 테스트 기능과 Firestore를 허용합니다.

    3단계 이미지 처리


    이미지를 처리할 때 ImageMagick을 사용하여 자세한 내용은 official Firebase tutorial을 참조하십시오.불행하게도 Image Magick 자체는 처음부터 배우기 어려웠다. 유행이 지난 것이 많기 때문에 솔직히 설명을 따르기 어려웠고, 게다가 제너럴 모터스도 유행이 지나 좋은 문서가 없었다.다행히도 저는 ImageMagick에 대한 익숙함과 원본 코드에 대한 깊은 이해로 이 문제를 찾게 되었습니다.
    이미지 처리는 다음 세 단계로 이루어집니다.
  • Larold image에서 사용해야 하는 제한된 이중 팔레트에 무단 색상을 매핑하기 위해 팔레트 이미지를 생성합니다.
  • 경고를 생성하기 위해 그림의 색 수량을 계산한다. 그러면 예술가들이 그림을 업데이트하려고 하면 그림이 틀렸다는 것을 알릴 수 있다
  • 이미지 크기를 조정하고 다시 비추어 버킷에 업로드합니다.
  • 단계 3.0 팔레트 이미지 생성


    우리는 단지 이렇게 한 번만 할 수 있다. 실제로 나는 이렇게 하려고 시도할 때 경쟁 위험에 부딪혔다. 왜냐하면 두 번의 교체는 팔레트를 동시에 생성하려고 시도하기 때문이다. 그래서 나는 그것을 상호 배척 대상에 포장해야 한다. (비동기적인 상호 배척 대상 npm 패키지를 통해)
    async function drawPalette() {
      const palettePath = "/tmp/palette.png";
    
      await paletteMutex.runExclusive(async () => {
        try {
          await fs.access(palettePath);
        } catch (error) {
          await new Promise((resolve, reject) => {
            gm(2, 1, "#1A1721FF")
                .fill("#FFC89C")
                .drawPoint(1, 0)
                .write(palettePath, (err, stdout) => {
                  if (err) {
                    reject(err);
                  } else {
                    functions.logger.info("Created palette file", {palettePath, stdout});
                    resolve(stdout);
                  }
                });
          });
        }
      });
    
      return palettePath;
    }
    
    이 함수는 gm/imagemagick에 2x1 픽셀의 PNG 파일을 그려야 합니다. 색 #1A1721과 #FFC89C 두 가지 larolds의 권한 있는 색을 포함합니다.

    3.2. 색상의 수량 계산


    gm/imagemagick의 identify() 함수는 그림에서 실제 사용하는 색을 빠르게 읽고 되돌려줍니다
    async function countColors(file) {
      return new Promise((resolve, reject) => {
        gm(file).identify("%k", (err, colors) => {
          if (err) {
            reject(err);
          } else {
            resolve(colors);
          }
        });
      });
    }
    

    3.3 그것을 처리한다


    다음 함수는 이 부분을 한데 묶고, Axios를 사용하여 URL에서 그림을 가져오고, 임시 파일에 쓰기, 크기 조정, 재매핑 변환, 버킷 저장소에 업로드하고, 생성된 경고를 되돌려줍니다.
    async function processImage(url, originalFilename, id) {
      const tempFileIn = `/tmp/${id}_${originalFilename}`;
      const tempFileOut = `/tmp/${id}.png`;
    
      // get file
      const res = await axios.get(url, {responseType: "arraybuffer"});
      await fs.writeFile(tempFileIn, res.data);
      functions.logger.info("Got file", {url, tempFileIn});
    
      // check colors
      const colors = await countColors(tempFileIn);
    
      // make palette
      const palettePath = await drawPalette();
    
      // do conversion
      await new Promise((resolve, reject) => {
        gm(tempFileIn)
            .resize(200, 200, ">")
            .in("-remap", palettePath)
            .write(tempFileOut, (err, stdout) => {
              if (err) {
                reject(err);
              } else {
                functions.logger.info("Processed image", {tempFileOut, stdout});
                resolve(stdout);
              }
            },
            );
      });
    
      // upload
      const destination = `larolds/${id}.png`;
      await bucket.upload(tempFileOut, {destination});
    
      // assemble warnings
      const warnings = [];
      if (colors != 2) {
        warnings.push(`Incorrect number of colors (${colors}) expected 2`);
      }
    
      await fs.unlink(tempFileIn);
      // await fs.unlink(tempFileOut); // might use this for cache
    
      functions.logger.info("Uploaded image", {destination, warnings});
      return {
        warnings,
        destination,
      };
    }
    
    엄밀히 말하면, 이것은 더욱 깨끗하게 하기 위해 더 많은 기능으로 분해되어야 한다.

    4단계:엘프 스틱 생성


    마지막으로 모든 이미지가 처리되어 안전하게 통에 업로드되면 우리는 요정 줄을 생성할 수 있다.
    이 코드는 2단계에서 만든 데이터 구조를 수신하거나, 저장통에서 그림을 꺼내거나, tmp 폴더에 남아 있는 처리된 출력 파일을 쉽게 찾을 수 있습니다
    async function makeComposite(laroldData) {
      // ensure images are downloaded
      const localPaths = await Promise.all(laroldData.map(async (doc) => {
        const localPath = `/tmp/${doc.id}.png`;
        try {
          await fs.access(localPath);
        } catch (error) {
          functions.logger.info("Downloading image", {destination: doc.destination});
          await bucket.file(doc.destination).download({destination: localPath});
        }
        return localPath;
      }));
    
      // montage
      const buffer = new Promise((resolve, reject) => {
        localPaths.slice(0, -1)
            .reduce((chain, localPath) => chain.montage(localPath), gm(localPaths[localPaths.length -1]))
            .geometry(200, 200)
            .in("-tile", "x1")
            .toBuffer("PNG", (err, buffer) => {
              if (err) {
                reject(err);
              } else {
                resolve(buffer);
              }
            },
            );
      });
    
      // cleanup
      await Promise.all(localPaths.map((localPath) => fs.unlink(localPath)));
    
      return buffer;
    }
    
    여기서 재미있는 일은 슬라이스와 Reduce를 사용하여 몽타주 이미지를 조합하는 데 필요한 방법 체인이다.세 폭의 그림에 대한 몽타주에 대해 코드는 보통 다음과 같다. gm(image2).montage(image0).montage(image1) 어떤 이유로 그림을 오른쪽 gm() 의 매개 변수에 놓는다.따라서 임의의 길이의 체인을 처리하기 위해 우리는 값에서 순환할 수 있다.
    let chain = gm(localPaths[localPaths.length -1]);
    for (let i = 0; i < localPaths.length-1; i++) {
      chain = chain.montage(localPaths[i]);
    }
    
    Reduce를 사용하여 다음과 같이 단순화할 수 있습니다.
    localPaths.slice(0, -1).reduce((chain, localPath) => chain.montage(localPath), gm(localPaths[localPaths.length -1]))
    

    5단계:zip 생성


    zip 파일 처리 jszip npm library 는 nodebuffer에서 zip을 다른 단계로 되돌려줍니다.Firebase 함수의express입니다.js가 실행될 때 바로 되돌아갈 수 있습니다.
      // generate composite and zip
      const zip = new JSZip();
      zip.file("larolds.json", JSON.stringify(laroldData, null, 2));
    
      if (laroldData.length > 0) {
        const compositeBuffer = await makeComposite(laroldData);
        zip.file(`larolds_strip${laroldData.length}.png`, compositeBuffer, {binary: true});
      }
    
      functions.logger.info("Done sync", {laroldData});
      return zip.generateAsync({type: "nodebuffer"});
    
    완성!나는 일부러 완전한 원본 파일을 포함하지 않는다. 왜냐하면 그것은 상당히 크기 때문이다. 그러나 위의 코드 예시가 Firebase 함수에서 gm/imagemagick을 사용하여 에어테이블의 이미지를 처리하기를 원하는 사람들에게 유용하기를 바란다.나는 실행할 때 필요한 메모리가Firebase 함수 설정 때의 기본 256MB보다 약간 많은 것을 발견했다. 현재 실행 속도는 512MB이지만, 더 큰 그림을 처리하기 위해 메모리를 늘려야 할 수도 있다.
    현재의 사용법은 필요할 때 zip 파일을 다운로드하는 것이지만, 미래의 교체 과정에서, 우리는 CI/CD로 하여금 이 zip 파일을 다운로드하고, 매번 main 지점에 통합하여 더욱 자동화하도록 리포에 제출할 수 있다.

    좋은 웹페이지 즐겨찾기