Convert base64 to PDF and save as an attachment

I’m working with an API that returns a JSON object from the the function below:

await remoteFetchAsync(url, getLabelOptions)
    .then((resp) => resp.json())
    .then(function(data) {
      pdfstring = data.label;
    })
    .then()
    .catch(function(e) {
        output.markdown("ERROR!");
        output.markdown(e.message);
    });

Considering the pdfstring is my base64 encoded pdf, how can I convert to pdf and save as an attachment?

In order to upload an attachment to Airtable, the file must exist at a publicly accessible url. You cannot create an attachment from a base64 encoded string.

Look for a service that will convert the base64 string back into a pdf file and also host the file at a publicly available url. There are several websites that convert base64 info back into a file. I’m not sure which have an api and will also host the file. If you find one, please report back!

Thank you, @kuovonne.
Super helpful.

Since a Base 64 file is just a standard text file, you can use any of the API services that convert TXT files to PDF files. Two good ones are:

https://cloudconvert.com/txt-to-pdf

You can provide either the text of your Base64 file, or you can point to the Base 64 file if it lives on a server somewhere.

Then, they provide you with a public URL with the URL of your actual PDF file, which you can use to download the attachment into Airtable.

1 Like

Thank you, @ScottWorld.

Already testing Cloudconvert.

Ok. Might need to tidy it up a bit but it works.
The script is using Cloudconvert API. The really cool part is that they will host the file for 24h.
No need to bring your own cloud storage service.
Any suggestions, how to make the code better, welcome!

// Click the "gear" icon in the top right to view settings
let config = input.config({
  title: "Your script with settings",
  description: "A script that uses settings",
  items: [
    input.config.table("selectedTable", {
      label: "Table to use",
      description: "Pick any table in this base!",
    }),
  ],
});

const BaseSpecificNames = {
  selectedTable: config.selectedTable,
  base64StringField: "Label Base64",
};

let cloudconvertToken = "YOUR TOKEN GOES HERE";

let selectedRecord = await input.recordAsync(
  "Select a record to use",
  BaseSpecificNames.selectedTable
);
let base64String = selectedRecord.getCellValueAsString(
  BaseSpecificNames.base64StringField
);

let url = "https://api.cloudconvert.com/v2/import/base64";
let payloadImport = {
  file: base64String,
  filename: "Label.pdf",
};

let postOptions = {
  method: "post",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${cloudconvertToken}`,
  },
  body: JSON.stringify(payloadImport),
};

//API Call: Create a task to import file from a base64 string.
let taskId = "";
await remoteFetchAsync(url, postOptions)
  .then((resp) => resp.json())
  .then(function (data) {
    output.inspect(data);
    taskId = data.data.id;
  })
  .catch(function (e) {
    output.markdown("ERROR!");
    output.markdown(e.message);
  });

//API Call: CREATE EXPORT URL TASKS; creates temporary URLs which can be used to download the files.
let convertURL = "https://api.cloudconvert.com/v2/export/url";

let payloadConvert = {
  input: taskId,
  archive_multiple_files: false,
};

let postOptionsConvert = {
  method: "post",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${cloudconvertToken}`,
  },
  body: JSON.stringify(payloadConvert),
};

await remoteFetchAsync(convertURL, postOptionsConvert)
  .then((resp) => resp.json())
  .then(function (data) {
    output.inspect(data);
    taskId = data.data.id;
  })
  .catch(function (e) {
    output.markdown("ERROR!");
    output.markdown(e.message);
  });

function timeout(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

await timeout(3000); //wait 3 seconds for the job to complete; note:might need to increase

//API Call: Retrieve the URL
let retrieveFileUrlPayload = {
  method: "get",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${cloudconvertToken}`,
  },
};

let convertedFileUrl = "";
let urlTaskId = `https://api.cloudconvert.com/v2/tasks/${taskId}`;
await remoteFetchAsync(urlTaskId, retrieveFileUrlPayload)
  .then((resp) => resp.json())
  .then(function (data) {
    output.inspect(data);
    convertedFileUrl = data.data.result.files[0].url;
  })
  .catch(function (e) {
    output.markdown("ERROR!");
    output.markdown(e.message);
  });

await BaseSpecificNames.selectedTable.updateRecordAsync(selectedRecord, {
  "PDF LABEL": [{ url: convertedFileUrl }],
});

Great job and congrats! :smiley: :raised_hands: :balloon:

And for those of you who can’t write Javascript code, you can do this same thing in a no-code environment using Integromat:

1 Like

Thank you for posting your solution. So glad you got it to work.

Here are a few thoughts regarding your code.

  • You use script settings for the table. I recommend also using script setting to select the field with the base64string and the attachment field. That way if the names of those fields change, your script will not break.

  • Keep in mind that your Cloudconvert token can be viewed by any collaborator who looks at the script.

  • You use the single variable taskId to refer to different tasks created across the course of the script. You might want to use different variable names to indicate that there are actually multiple tasks.

  • Consider breaking up the script into smaller functions that get called by a single main function. This will help make the general flow of the script easier to see and maintain. It will also make it easier to deal gracefully with any failed api calls. Functions can also make your code a bit more “dry”, but leaving it “wet” is fine if you understand it better that way.

  • Your comments indicate that the timeout might need to be increased. You could put the timeout in a loop so that you can keep checking the status of the task until it is complete.

1 Like

Thank you, that’s very helpful @kuovonne.
One of mi biggest struggles in learning JS is making my code less DRY.
Repurposing taskId was a bit silly, thinking about it now :see_no_evil:

This topic was solved and automatically closed 3 days after the last reply. New replies are no longer allowed.