Skip to content

Instantly share code, notes, and snippets.

@simonw
Created March 30, 2024 17:11
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save simonw/e58796324abb0e729b2dcd351f46728a to your computer and use it in GitHub Desktop.
Save simonw/e58796324abb0e729b2dcd351f46728a to your computer and use it in GitHub Desktop.

2024-03-24T00:24:24 conversation: 01hspxh7dnmvcbpy11cbhv44cg

Model: claude-3-opus-20240229

Prompt:

i have a long running JavaScript task which spits out a number of different images. I want to design an API around this using promises. If it only returned one image I would have it resolve the promise when that image is ready, but I am not sure how to design it for multiple images in a sequence with gaps in between. Suggest several options for this design.

Response:

When designing an API for a long-running JavaScript task that generates multiple images with gaps in between, you have several options to consider. Here are a few approaches you can take:

  1. Promise-based with multiple resolves:

    • Create a single promise that resolves multiple times, once for each image generated.
    • Use a method like notify or emit to send the generated images as they become available.
    • The promise finally resolves when all images have been generated.
    • The client can listen for the notify or emit events to receive the images as they are ready.
  2. Promise-based with an array of results:

    • Create a promise that resolves once when all images have been generated.
    • The promise resolves with an array containing all the generated images.
    • The client receives the array of images upon promise resolution.
  3. Async iterator or generator:

    • Use an async iterator or generator function to yield the images as they are generated.
    • The client can iterate over the async iterator or generator to retrieve the images one by one.
    • The iterator or generator completes when all images have been yielded.
  4. Observable-based:

    • Use an observable pattern, such as RxJS or a similar library.
    • The long-running task emits the generated images as observable events.
    • The client subscribes to the observable to receive the images as they are emitted.
    • The observable completes when all images have been emitted.
  5. Callback-based:

    • Accept a callback function as a parameter to the API method.
    • Invoke the callback function each time an image is generated, passing the image as an argument.
    • The client provides the callback function to handle the received images.
    • Optionally, you can include a final callback or a completion event to signal when all images have been generated.

Here's an example of the promise-based approach with multiple resolves:

function generateImages() {
  return new Promise((resolve, reject) => {
    // Start the long-running task
    // ...

    // When an image is generated
    function onImageGenerated(image) {
      // Notify the client with the generated image
      resolve({ value: image, done: false });
    }

    // When the task is completed
    function onTaskCompleted() {
      // Resolve the promise with a done flag
      resolve({ done: true });
    }

    // ...
  });
}

// Client code
generateImages()
  .then(({ value, done }) => {
    if (!done) {
      // Handle the generated image
      console.log('Received image:', value);
    } else {
      console.log('Task completed');
    }
  })
  .catch((error) => {
    console.error('Error:', error);
  });

In this example, the generateImages function returns a promise that resolves multiple times using resolve({ value: image, done: false }) for each generated image. The client receives the images by chaining .then() to the promise and checking the done flag to determine if more images are expected or if the task is completed.

Choose the approach that best fits your specific requirements and the conventions of your application.

2024-03-24T00:25:11

Prompt:

show me an example of an async iterator, both the client and the API implementation itself

Response:

Certainly! Here's an example of an async iterator implementation for generating images and the corresponding client code:

API Implementation:

async function* generateImages() {
  // Simulating a long-running task that generates images
  for (let i = 1; i <= 5; i++) {
    // Simulating image generation delay
    await new Promise((resolve) => setTimeout(resolve, 1000));

    // Yielding the generated image
    yield `image${i}.jpg`;
  }
}

In this example, the generateImages function is an async generator function. It uses a for loop to simulate generating five images. Each iteration of the loop includes a delay of one second using setTimeout to simulate the time taken to generate an image. The yield keyword is used to yield each generated image.

Client Code:

async function processImages() {
  const imageIterator = generateImages();

  try {
    for await (const image of imageIterator) {
      console.log('Received image:', image);
      // Process the received image
      // ...
    }
    console.log('Task completed');
  } catch (error) {
    console.error('Error:', error);
  }
}

// Start processing images
processImages();

In the client code, the processImages function is an async function that initiates the image processing. It calls the generateImages function to obtain an async iterator.

Inside the processImages function, a for await...of loop is used to iterate over the async iterator. Each iteration retrieves the next image yielded by the iterator using the await keyword. The received image is then logged to the console, and you can perform any necessary processing or handling of the image.

If an error occurs during the iteration, it will be caught by the catch block, and the error will be logged to the console.

When you run the client code, you will see the following output:

Received image: image1.jpg
Received image: image2.jpg
Received image: image3.jpg
Received image: image4.jpg
Received image: image5.jpg
Task completed

The async iterator approach allows the client to retrieve the images one by one as they are generated, without waiting for all the images to be generated upfront. The client can start processing each image as soon as it is received, providing a more responsive and efficient handling of the long-running task.

Note that the example uses a simulated delay to represent the image generation process. In a real scenario, you would replace the delay with the actual logic for generating or obtaining the images.

2024-03-24T00:28:54

Prompt:

Now write code that does the following. A function is called to start the process, and passed a file object (e.g. from a input type=file widget). That function then spins off a web worker - using the pattern where the web worker code is defined in a backtick literal and then used with a base64 URL to start the worker.

That web worker gets sent the file (or the content of that file if it is not possible to pass file objects across the interface between browser and web worker).

It loads PDF.js from a CDN and uses it to parse the file - sending back an error if it cannot be parsed

Then it starts looping through each page in the PDF, for each one it renders that page as a 400px wide (and as tall as it should be based on the aspect ratio) JPG. That JPEG is messaged back to the parent page.

When it has finished, it sends a message saying it has finished

The parent page then destroys the webworker

The interface to all of this should be an async iterator as demonstrated before

Build this as a single page of HTML and embedded JavaScript. The HTML should include an input type=file and when that file is attached it should kick off the process.

The images should be appended to a div below the file input as they become available from the worker.'

Response:

Here's an example implementation of the described functionality in a single HTML page with embedded JavaScript:

<!DOCTYPE html>
<html>
<head>
  <title>PDF to Images</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.9.359/pdf.min.js"></script>
  <style>
    .image-container img {
      margin-bottom: 10px;
    }
  </style>
</head>
<body>
  <input type="file" id="fileInput" accept=".pdf" />
  <div class="image-container"></div>

  <script>
    const fileInput = document.getElementById('fileInput');
    const imageContainer = document.querySelector('.image-container');

    fileInput.addEventListener('change', handleFileUpload);

    async function handleFileUpload(event) {
      const file = event.target.files[0];
      const imageIterator = convertPDFToImages(file);

      for await (const image of imageIterator) {
        const imgElement = document.createElement('img');
        imgElement.src = image;
        imageContainer.appendChild(imgElement);
      }
    }

    async function* convertPDFToImages(file) {
      const worker = new Worker(URL.createObjectURL(new Blob([`
        self.importScripts('https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.9.359/pdf.worker.min.js');

        self.onmessage = async function(event) {
          const file = event.data;
          try {
            const pdf = await pdfjsLib.getDocument(file).promise;
            const numPages = pdf.numPages;

            for (let i = 1; i <= numPages; i++) {
              const page = await pdf.getPage(i);
              const viewport = page.getViewport({ scale: 1 });
              const canvas = new OffscreenCanvas(400, (400 / viewport.width) * viewport.height);
              const context = canvas.getContext('2d');
              const renderContext = {
                canvasContext: context,
                viewport: viewport
              };
              await page.render(renderContext).promise;
              const imageData = await canvas.convertToBlob({ type: 'image/jpeg', quality: 0.8 });
              self.postMessage(URL.createObjectURL(imageData));
            }

            self.postMessage(null);
          } catch (error) {
            self.postMessage({ error: error.message });
          }
        };
      `], { type: 'application/javascript' })));

      worker.postMessage(file);

      while (true) {
        const message = await new Promise((resolve) => {
          worker.onmessage = (event) => resolve(event.data);
        });

        if (message === null) {
          break;
        } else if (message.error) {
          throw new Error(message.error);
        } else {
          yield message;
        }
      }

      worker.terminate();
    }
  </script>
</body>
</html>

Explanation:

  1. The HTML structure includes an input element of type file with an accept attribute set to .pdf to restrict file selection to PDF files.

  2. The handleFileUpload function is triggered when a file is selected. It retrieves the selected file and calls the convertPDFToImages function, which returns an async iterator.

  3. The for await...of loop iterates over the async iterator, creating an img element for each received image URL and appending it to the imageContainer div.

  4. The convertPDFToImages function creates a new Web Worker using a Blob URL. The Web Worker code is defined as a string literal within backticks.

  5. The Web Worker imports the PDF.js library from a CDN and sets up an onmessage event handler to receive the file.

  6. Inside the Web Worker, the PDF file is loaded using pdfjsLib.getDocument. If an error occurs, it is sent back to the main thread using postMessage.

  7. The Web Worker iterates over each page of the PDF, renders it to an OffscreenCanvas with a width of 400px and a proportional height based on the aspect ratio.

  8. The rendered page is converted to a JPEG Blob using canvas.convertToBlob and sent back to the main thread as a URL using postMessage.

  9. After processing all pages, the Web Worker sends a null message to indicate completion and terminates itself.

  10. The main thread receives the image URLs and the completion message using the onmessage event handler. It yields the image URLs and breaks the loop when the completion message is received.

  11. Finally, the Web Worker is terminated using worker.terminate().

This code provides a simple interface where the user can select a PDF file, and the images generated from each page of the PDF are appended to the imageContainer div as they become available from the Web Worker.

2024-03-24T00:31:01

Prompt:

Uncaught (in promise) Error: pdfjsLib is not defined - got that error in the webworker so it looks like the importscripts is not quite right. just tell me what to change to fix it

Response:

Apologies for the confusion. The issue is that the pdfjsLib object is not directly available in the Web Worker scope after importing the PDF.js library. To fix this, you need to use self.pdfjsLib instead of pdfjsLib inside the Web Worker.

Please update the following line in the Web Worker code:

const pdf = await pdfjsLib.getDocument(file).promise;

to:

const pdf = await self.pdfjsLib.getDocument(file).promise;

So, the corrected Web Worker code should look like this:

self.importScripts('https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.9.359/pdf.worker.min.js');

self.onmessage = async function(event) {
  const file = event.data;
  try {
    const pdf = await self.pdfjsLib.getDocument(file).promise;
    // ... rest of the code ...
  } catch (error) {
    self.postMessage({ error: error.message });
  }
};

With this change, the Web Worker should be able to access the pdfjsLib object correctly using self.pdfjsLib, and the error should be resolved.

2024-03-24T00:33:45

Prompt:

actually it looks like pdf.js has its own mechanism for running web workers. rewrite the whole HTML page to use that mechanism instead, rather than running its own web worker

Response:

Certainly! Here's the updated HTML page that uses the built-in Web Worker mechanism provided by PDF.js:

<!DOCTYPE html>
<html>
<head>
  <title>PDF to Images</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.9.359/pdf.min.js"></script>
  <style>
    .image-container img {
      margin-bottom: 10px;
    }
  </style>
</head>
<body>
  <input type="file" id="fileInput" accept=".pdf" />
  <div class="image-container"></div>

  <script>
    const fileInput = document.getElementById('fileInput');
    const imageContainer = document.querySelector('.image-container');

    fileInput.addEventListener('change', handleFileUpload);

    pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.9.359/pdf.worker.min.js';

    async function handleFileUpload(event) {
      const file = event.target.files[0];
      const imageIterator = convertPDFToImages(file);

      for await (const image of imageIterator) {
        const imgElement = document.createElement('img');
        imgElement.src = image;
        imageContainer.appendChild(imgElement);
      }
    }

    async function* convertPDFToImages(file) {
      try {
        const pdf = await pdfjsLib.getDocument(URL.createObjectURL(file)).promise;
        const numPages = pdf.numPages;

        for (let i = 1; i <= numPages; i++) {
          const page = await pdf.getPage(i);
          const viewport = page.getViewport({ scale: 1 });
          const canvas = document.createElement('canvas');
          const context = canvas.getContext('2d');
          canvas.width = 400;
          canvas.height = (400 / viewport.width) * viewport.height;
          const renderContext = {
            canvasContext: context,
            viewport: page.getViewport({ scale: 400 / viewport.width }),
          };
          await page.render(renderContext).promise;
          const imageURL = canvas.toDataURL('image/jpeg', 0.8);
          yield imageURL;
        }
      } catch (error) {
        console.error('Error:', error);
      }
    }
  </script>
</body>
</html>

Explanation of the changes:

  1. The separate Web Worker code has been removed, and the PDF.js library is now used directly in the main script.

  2. The pdfjsLib.GlobalWorkerOptions.workerSrc is set to the URL of the PDF.js worker script. This tells PDF.js to use its own Web Worker for processing PDF files.

  3. Inside the convertPDFToImages function, the PDF file is loaded using pdfjsLib.getDocument by passing the URL of the file object.

  4. The function iterates over each page of the PDF, creates a canvas element, and renders the page to the canvas using page.render.

  5. The rendered page is converted to a JPEG image URL using canvas.toDataURL, and the image URL is yielded.

  6. If an error occurs during the PDF processing, it is caught and logged to the console.

  7. The rest of the code remains the same, where the handleFileUpload function retrieves the selected file, calls convertPDFToImages, and appends the generated image URLs to the imageContainer div.

With these changes, the PDF.js library will handle the PDF processing using its own Web Worker mechanism, eliminating the need for a separate Web Worker setup. The generated images will be appended to the imageContainer div as they become available.

2024-03-24T00:35:45

Prompt:

output the whole HTML page again, this time including a KB formatted size of each image below each image

Response:

Here's the updated HTML page that includes the size of each image in kilobytes (KB) below each generated image:

<!DOCTYPE html>
<html>
<head>
  <title>PDF to Images</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.9.359/pdf.min.js"></script>
  <style>
    .image-container img {
      margin-bottom: 10px;
    }
    .image-container p {
      margin: 0;
      font-size: 14px;
      color: #888;
    }
  </style>
</head>
<body>
  <input type="file" id="fileInput" accept=".pdf" />
  <div class="image-container"></div>

  <script>
    const fileInput = document.getElementById('fileInput');
    const imageContainer = document.querySelector('.image-container');

    fileInput.addEventListener('change', handleFileUpload);

    pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.9.359/pdf.worker.min.js';

    async function handleFileUpload(event) {
      const file = event.target.files[0];
      const imageIterator = convertPDFToImages(file);

      for await (const { imageURL, size } of imageIterator) {
        const imgElement = document.createElement('img');
        imgElement.src = imageURL;
        imageContainer.appendChild(imgElement);

        const sizeElement = document.createElement('p');
        sizeElement.textContent = `Size: ${formatSize(size)}`;
        imageContainer.appendChild(sizeElement);
      }
    }

    async function* convertPDFToImages(file) {
      try {
        const pdf = await pdfjsLib.getDocument(URL.createObjectURL(file)).promise;
        const numPages = pdf.numPages;

        for (let i = 1; i <= numPages; i++) {
          const page = await pdf.getPage(i);
          const viewport = page.getViewport({ scale: 1 });
          const canvas = document.createElement('canvas');
          const context = canvas.getContext('2d');
          canvas.width = 400;
          canvas.height = (400 / viewport.width) * viewport.height;
          const renderContext = {
            canvasContext: context,
            viewport: page.getViewport({ scale: 400 / viewport.width }),
          };
          await page.render(renderContext).promise;
          const imageURL = canvas.toDataURL('image/jpeg', 0.8);
          const size = calculateSize(imageURL);
          yield { imageURL, size };
        }
      } catch (error) {
        console.error('Error:', error);
      }
    }

    function calculateSize(imageURL) {
      const base64Length = imageURL.length - 'data:image/jpeg;base64,'.length;
      const sizeInBytes = Math.ceil(base64Length * 0.75);
      return sizeInBytes;
    }

    function formatSize(size) {
      const sizeInKB = (size / 1024).toFixed(2);
      return `${sizeInKB} KB`;
    }
  </script>
</body>
</html>

Explanation of the changes:

  1. The convertPDFToImages function now yields an object containing both the imageURL and the size of each generated image.

  2. The calculateSize function is added to calculate the size of the image in bytes based on the base64-encoded image URL. It removes the data URL prefix, calculates the size in bytes using the formula base64Length * 0.75, and returns the size.

  3. The formatSize function is added to format the size in bytes to kilobytes (KB) with two decimal places.

  4. Inside the handleFileUpload function, the for await...of loop now destructures the yielded object to get the imageURL and size.

  5. After appending the image element to the imageContainer, a new p element is created to display the size of the image in KB.

  6. The size is formatted using the formatSize function and appended to the imageContainer below each image.

  7. The CSS styles are updated to style the size text below each image.

With these changes, the generated images will be displayed along with their sizes in KB below each image. The sizes are calculated based on the base64-encoded image URLs and formatted for display.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment