将 dataURL/Buffer 转换为 blob

Convert dataURL/Buffer to blob

我正在使用接受 Blob 格式的 npm canvas package and I want to upload my canvas image data using an API

最初我用它来将我的 dataUrl 格式转换为 blob:

const dataUrl = canvas.toDataURL();
console.log('[debug] dataUrl', dataUrl); // [debug] dataUrl data:image/png;base64,iVBORw0KGgoAAAAN......

...
...

const res = await fetch(dataUrl);
const blob = await res.blob();
console.log('blob', blob); // some Blob

我将框架迁移到 remix.js,当我尝试 运行 这段代码时,出现错误:

TypeError: Only HTTP(S) protocols are supported

Canvas 可以将图像导出为 dataURL 或 Buffer,但我似乎无法弄清楚如何将它们中的任何一个转换为 Blob,任何帮助将不胜感激

Buffer -> <Buffer 66 00 68 00 71 00 77 00 68 00 67 00 61 00 64 00 73 00>

DataURL -> data:image/png;base64,iVBORw0KGgoAAAAN......

这是一个完整的示例,说明如何从 canvas 获取 Pinata 文件上传结果。我已经包含了很多评论,如果有不清楚的地方可以进一步解释:

文件:


./package.json:

{
  "name": "so-71458029",
  "version": "0.1.0",
  "description": "",
  "type": "module",
  "scripts": {
    "compile": "tsc",
    "dev": "ts-node --esm src/main.ts",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "MIT",
  "devDependencies": {
    "@types/node": "^17.0.21",
    "ts-node": "^10.7.0",
    "typescript": "^4.6.2"
  },
  "dependencies": {
    "canvas": "^2.9.0",
    "dotenv": "^16.0.0",
    "form-data": "^4.0.0",
    "node-fetch": "^3.2.3"
  }
}


./tsconfig.json:

{
  "compilerOptions": {
    // Node 16 ESM options
    // Ref: https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping#node-16
    "lib": ["es2021"],
    "module": "esnext",
    "moduleResolution": "node",
    "target": "es2021",

    // Compatibility
    "allowSyntheticDefaultImports": true,

    // Suggestion: strict options for safety
    "strict": true,
    "exactOptionalPropertyTypes": true,
    "isolatedModules": true,
    "noUncheckedIndexedAccess": true,
    "useUnknownInCatchVariables": true,

    // output config
    "declaration": true,
    "outDir": "dist",
  },
  "include": [
    "./src/**/*"
  ]
}


./.env:

PINATA_JWT=your_actual_pinata_jwt


./src/canvas.ts:

/*
import {createCanvas, type Canvas} from 'canvas';
        ^^^^^^^^^^^^
SyntaxError: Named export 'createCanvas' not found.
The requested module 'canvas' is a CommonJS module,
which may not support all module.exports as named exports.
*/
import _canvas, {type Canvas} from 'canvas';
import FormData from 'form-data';
import {Headers} from 'node-fetch';

const {createCanvas} = _canvas;

/** https://github.com/form-data/form-data/tree/v4.0.0#usage */
export function createRequestInitFromCanvas (canvas: Canvas): {
  body: FormData;
  headers: Headers;
} {
  const form = new FormData();

  // Stream the canvas image data. (Other formats available.)
  // https://github.com/Automattic/node-canvas/tree/v2.9.0#canvascreatepngstream
  const pngStream = canvas.createPNGStream();

  const fileOptions: FormData.AppendOptions = {
    contentType: 'image/png',
    filename: 'canvas.png',
  };

  form.append('file', pngStream, fileOptions);

  return {
    body: form,
    headers: new Headers(form.getHeaders()),
  };
}

/** https://github.com/Automattic/node-canvas/tree/v2.9.0#quick-example */
export function getExampleCanvas (): Canvas {
  const canvas = createCanvas(200, 200);
  const ctx = canvas.getContext('2d');

  // Write "Awesome!"
  ctx.font = '30px Impact';
  ctx.rotate(0.1);
  ctx.fillText('Awesome!', 50, 100);

  // Draw line under text
  const text = ctx.measureText('Awesome!');
  ctx.strokeStyle = 'rgba(0,0,0,0.5)';
  ctx.beginPath();
  ctx.lineTo(50, 102);
  ctx.lineTo(50 + text.width, 102);
  ctx.stroke();

  return canvas;
}


./src/main.ts:

import 'dotenv/config';
import assert from 'assert/strict';
import {type Canvas} from 'canvas';
import {default as fetch, Request, Response} from 'node-fetch';
import {createRequestInitFromCanvas, getExampleCanvas} from './canvas.js';

async function createResponseError (response: Response): Promise<Error> {
  let body: any = '';

  try {
    body = await response.text();
    body = JSON.parse(body);
  }
  catch {/* empty */}

  if (!body) body = null;

  const json = JSON.stringify({
    status: response.status,
    headers: Object.fromEntries([...response.headers].sort()),
    body,
  }, null, 2);

  return new Error(`Response error:\n${json}`);
}

/** https://docs.pinata.cloud/api-pinning/pin-file#response */
type PinataFileUploadResponse = {
  IpfsHash: string;
  PinSize: number;
  /** ISO 8601 datetime */
  Timestamp: string;
};

/** https://docs.pinata.cloud/api-pinning/pin-file */
async function pinCanvasImage (pinataJWT: string, canvas: Canvas): Promise<PinataFileUploadResponse> {
  const {body, headers} = createRequestInitFromCanvas(canvas);

  // https://docs.pinata.cloud/#connecting-to-the-api
  // https://docs.pinata.cloud/#your-api-keys
  headers.set('Authorization', `Bearer ${pinataJWT}`);

  // See more body options and examples at:
  // https://docs.pinata.cloud/api-pinning/pin-file#body
  
  // https://docs.pinata.cloud/api-pinning/pin-file#pinataoptions-optional
  // const pinataOptions = {};
  // body.append('pinataOptions', JSON.stringify(pinataOptions));
  
  // https://docs.pinata.cloud/api-pinning/pin-file#pinatametadata-optional
  // const pinataMetadata = {};
  // body.append('pinataMetadata', JSON.stringify(pinataMetadata));

  const request = new Request('https://api.pinata.cloud/pinning/pinFileToIPFS', {
    method: 'POST',
    body,
    headers,
  });

  const response = await fetch(request);
  if (!response.ok) {
    const err = await createResponseError(response);
    throw err;
  }
  return response.json() as Promise<PinataFileUploadResponse>;
}

async function main () {
  const {PINATA_JWT} = process.env;

  // Ensure that the environment variable exists
  // https://nodejs.org/api/assert.html#assertokvalue-message
  assert.ok(PINATA_JWT);

  const canvas = getExampleCanvas();

  const result = await pinCanvasImage(PINATA_JWT, canvas);
  console.log(result);
}

main();


运行它::

Node/npm 版本:

$ node --version
v16.14.0
$ npm --version
8.3.1

运行 使用:

$ npm install
$ npm run dev

对于正在寻找 axios 版本答案的任何人,可以在此处找到 form-data 与 axios 的用法:https://www.npmjs.com/package/form-data

interface PinataMetadata {
    IpfsHash: string;
    PinSize: number;
    Timestamp: string | number;
}

const PinataInstance = axios.create({
    baseURL: 'https://api.pinata.cloud',
    headers: {
        pinata_api_key: key,
        pinata_secret_api_key: secret
    }
});

export const pinFileToIPFS = async (pngStream: PNGStream) => {
    const path = '/pinning/pinFileToIPFS';
    const form = new FormData();

    const fileOptions: FormData.AppendOptions = {
        contentType: 'image/png',
        filename: 'canvas.png'
    };

    form.append('file', pngStream, fileOptions);

    try {
        const res = await PinataInstance.post<PinataMetadata>(path, form, {
            headers: { ...form.getHeaders() }
        });

        return 'https://gateway.pinata.cloud/ipfs/' + res.data.IpfsHash;
    } catch (error: any) {
        console.error('Pinata error: ', error);
    }
};