Angular 13: Convert J2K to PNG

177 Views Asked by At

Good day,

Is there a way I could convert JPEG2000 base64 data to PNG data in Angular so I could display it on the browser? Here's a test component to give you an idea of what I need:

my-component.component.ts


import { Component, OnInit } from '@angular/core';

@Component({
    selector: 'dha-my-component',
    templateUrl: './my-component.component.html',
    styleUrls: ['./my-component.component.scss']
})
export class MyComponentComponent implements OnInit {
    imageData: string;
    constructor() {}

    ngOnInit(): void {
        const testJ2KImage = '/0//UQA...';
        this.imageData = this.convertJ2kToPng(testJ2KImage);
    }

    convertJ2kToPng(base64Data: string) {
        // Convert base 64 data from JPEG2000 to PNG
        const newPngImageData = 'iVBOR...';

        return newPngImageData;
    }
}

my-component.component.html

<img [src]="'data:image/png;base64,' + imageData" class="img img-responsive" />
1

There are 1 best solutions below

0
skink On

Normally it's done via a backend call, however, it's possible (albeit tricky) to do so in the browser.

First of all, let's decode the input image. There doesn't seem to be a usable NPM package that does it in the browser, so we'll use j2k.js and load it directly:

import { fromByteArray, toByteArray } from 'base64-js';

declare function openjpeg(data: Uint8Array, suffix: 'jp2' | 'jk2'): { width: number; height: number; data: Uint8Array };

ngOnInit(): void {
  this.loadOpenJpeg().subscribe(() => {
    this.imageData = this.convertJ2kToPng(testJ2KImage);
  });
}

private loadOpenJpeg(): Observable<void> {
  const script = document.createElement('script');
  script.src = 'https://cdn.jsdelivr.net/gh/kripken/j2k.js@master/openjpeg.js';
  script.async = false;
  document.head.appendChild(script);
  return new Observable((sub) => {
    script.onload = () => {
      sub.next();
      sub.complete();
    };
  });
}

private convertJ2kToPng(base64Data: string): string {
  const jpeg = openjpeg(toByteArray(base64Data), 'jp2');
  // todo
  return fromByteArray(null);
}

Then let's convert the decoded input image to PNG. We'll use the fast-png package for that:

import { encode as pngencode } from 'fast-png';

private convertJ2kToPng(base64Data: string): string {
  const jpeg = openjpeg(toByteArray(base64Data), 'jp2');
  const png = pngencode({
    width: jpeg.width,
    height: jpeg.height,
    data: this.planarToPacked(jpeg.data, jpeg.width, jpeg.height),
    channels: 3,
  });
  return fromByteArray(png);
}

// openjpeg decoded pixels are in 24-bit planar format, but fast-png wants them in packed format
private planarToPacked(input: Uint8Array, width: number, height: number): Uint8Array {
  const plane = width * height;
  const output = new Uint8Array(input.length);
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      const i = y * width + x;
      output[i * 3] = input[i];
      output[i * 3 + 1] = input[plane + i];
      output[i * 3 + 2] = input[plane * 2 + i];
    }
  }
  return output;
}

StackBlitz


Alternatively, this would be the backend way:

@Component({
  imports: [CommonModule, HttpClientModule],
  selector: 'app-root',
  template: `<img *ngIf="imageData" [src]="imageData" />`,
  standalone: true,
})
export class AppComponent implements OnInit {
  protected imageData: SafeResourceUrl;

  constructor(
    private readonly httpClient: HttpClient,
    private readonly sanitizer: DomSanitizer
  ) {}

  ngOnInit(): void {
    this.convertJ2kToPng(TestJ2KImage).subscribe((imageData) => {
      this.imageData = this.sanitizer.bypassSecurityTrustResourceUrl(
        'data:image/png;base64,' + imageData
      );
    });
  }

  private convertJ2kToPng(base64Data: string): Observable<string> {
    return this.httpClient
      .post<{ data: string }>('http://localhost:3000/convert', { data: base64Data })
      .pipe(map((x) => x.data));
  }
}

I decided to set up a simple Express backend that uses jpeg2000 and pngjs for the image conversion:

// jpeg2000 pixels are RGB but pngjs expects RGBA
function rgbToRgba(input: Uint8Array): Uint8Array {
  const output = new Uint8Array((input.length / 3) * 4);
  for (let x = 0; x < input.length / 3; x++) {
    output[x * 4] = input[x * 3];
    output[x * 4 + 1] = input[x * 3 + 1];
    output[x * 4 + 2] = input[x * 3 + 2];
    output[x * 4 + 3] = 255;
  }
  return output;
}

app.post('/convert', (req: Request, res: Response) => {
  const jpx = new JpxImage();
  jpx.parse(Buffer.from(req.body.data, 'base64'));
  const png = new PNG({ width: jpx.width, height: jpx.height });
  png.data = Buffer.from(rgbToRgba(jpx.tiles[0].items));
  res.send({ data: PNG.sync.write(png).toString('base64') });
});

StackBlitz -- please navigate to https://angularwebcontainertemplateddn-livb--4200--95b70c8d.local-credentialless.webcontainer.io when both applications are started.