Downloading a PDF with fabric.js edits included not working

I have an Angular 16 and Laravel 11 web application and I'm working on a component where users can make edits on a PDF file then download that edited PDF file on their computer. For that I used pdf.js for viewing and fabric.js for editing and pdf-lib for downloading the PDF here are the versions in case:

"fabric": "^6.5.3",  
"pdf-lib": "^1.17.1",
"pdfjs-dist": "^3.11.174",

So all is good (editing with fabric, saving edits..) except for the download functionality, the PDF is being downloaded without the different edits of each page, I don't know what is wrong with my logic and since I'm a beginner in TypeScript I don't seem to find out the error, could someone with experience see my code and help me fix it please!

Here is my ts file:

import { Component, ElementRef, ViewChild} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { EditorService } from 'src/app/services/editor.service';
import * as pdfjsLib from 'pdfjs-dist';
import * as fabric from 'fabric';
import { EraserBrush } from './eraser-brush';
import { PDFDocument } from 'pdf-lib';
import jsPDF from 'jspdf';


declare global {
  interface Window { pdfjsWorker: any; }
}
pdfjsLib.GlobalWorkerOptions.workerSrc = '/assets/pdf.worker.min.js'; // Ensure this path is correct


@Component({
  selector: 'app-pdf-editor',
  templateUrl: './pdf-editor.component.html',
  styleUrls: ['./pdf-editor.component.css']
})

export class PdfEditorComponent {

  currentTool: 'hand' | 'select' = 'select'; // Initialize with 'hand'
  currentPen: 'pencel' | 'highlighter' = 'pencel'; // Initialize with 'hand'

  @ViewChild('pdfCanvas', { static: false }) pdfCanvas!: ElementRef;
  @ViewChild('fabricCanvas', { static: false }) fabricCanvas!: ElementRef;
  @ViewChild('fileInput') fileInput!: ElementRef;

  file_id: string = '';
  file_type: string = '';
  pdfDoc: any;
  currentPage = 1;
  totalPages: number = 0;
  pageStates: { [key: number]: any } = {}; // Store fabric states per page
  pdfURL: any;

  selectedTextColor: string = '#000000';
  selectedSize: number = 26;
  selectedFont: string = 'Arial';
  selectedColor: string = '#000000';
  recentColors: string[] = ['#000000'];

  showColorPalette: boolean = false;
  showFontOptions: boolean = false;

  canvas!: fabric.Canvas;
  fabricInstance: fabric.Canvas = new fabric.Canvas();

  showLinkModal = false; // Controls the visibility of the modal
  linkText = ''; // Stores the link text entered by the user
  linkUrl = ''; // Stores the link URL entered by the user

  isSaved: boolean = false;
  saveMessage: string = '';

  constructor(private route: ActivatedRoute, private pdfService: EditorService) { }

  ngAfterViewInit(): void {
    //retreive the selected PDF file id to in order to display it
    this.route.params.subscribe((params) => {
      this.file_id = params['file_id'];
      this.file_type = params['file_type'];
      this.fetchPdfFile(this.file_id, this.file_type);
    });

    //initialize the drawing canvas of fabric.js
    this.canvas = new fabric.Canvas('annotationCanvas', {
      width: this.pdfCanvas.nativeElement.width, // Match PDF canvas width
      height: this.pdfCanvas.nativeElement.height, // Match PDF canvas height
      preserveObjectStacking: false, // Ensure correct object stacking
      selection: false, // Disable object selection (optional)
      interactive: true, // Enable interaction (drawing, moving)
    });

    // Set dimensions for the Fabric canvas
    this.canvas.setDimensions({
      width: this.pdfCanvas.nativeElement.width,
      height: this.pdfCanvas.nativeElement.height
    });

    // Allow object selection and manipulation
    this.canvas.on('selection:created', () => {
      const activeObject = this.canvas.getActiveObject();
      this.canvas.renderAll();

      if (activeObject) {
        if (activeObject.type === 'text') {
          this.selectedColor = (activeObject as fabric.Textbox).fill as string;
        } else {
          this.selectedColor = (activeObject as any).stroke || '#000000';
        }
      }
    });

    this.canvas.on('selection:updated', () => {
      const activeObject = this.canvas.getActiveObject();
      this.canvas.renderAll();

      if (activeObject) {
        if (activeObject.type === 'text') {
          this.selectedColor = (activeObject as fabric.Textbox).fill as string;
        } else {
          this.selectedColor = (activeObject as any).stroke || '#000000';
        }
      }
    });

    this.canvas.on('object:moving', () => {
      requestAnimationFrame(() => this.canvas.renderAll());
    });

    this.canvas.on('object:modified', () => {
      this.canvas.renderAll();
    });

    this.canvas.on('object:added', () => {
     // this.saveCanvasState();
      this.canvas.renderAll(); // Force a redraw to ensure visibility
    });

   // this.canvas.on('object:added', () => this.saveCanvasState());

  }

  ngOnDestroy() {
    if (this.canvas) {
      this.canvas.dispose();
      this.fabricInstance.dispose();
    }
  }


  saveCanvasState(): void {
    const state = JSON.stringify(this.canvas);
    this.pageStates[this.currentPage] = state;
    console.log('Saved state:', state);

    const formData = new FormData();
    formData.append('file_id', this.file_id); // Ensure file_id is a string
    formData.append('file_type', this.file_type); // Ensure file_id is a string
    formData.append('page_number', this.currentPage.toString()); // Convert currentPage to string
    formData.append('canvas_state', state);

    console.log(formData);
    this.pdfService.saveEdits(formData).subscribe(
      (response) => {
        if (response.success) {
          this.isSaved = true;
          this.saveMessage = 'File saved!';
          setTimeout(() => (this.isSaved = false), 3000); // Hide the message after 3 seconds

        } else {
          console.error('Error saving edits:', response.error);
        }
      },
      (error) => console.error('Error saving edits:', error)
    );
  }

  restoreCanvasState(): void {
    const state = this.pageStates[this.currentPage];
    if (state) {
      this.canvas.clear(); // Clear canvas before loading the new state
      this.canvas.loadFromJSON(state, () => {
        this.canvas.renderAll(); // Render canvas immediately
      });
    } else {
      this.canvas.clear(); // Clear if no state exists for the page
    }
  }


  fetchPdfFile(file_id: string, file_type:string): void {
    this.pdfService.loadFile(file_id,file_type).subscribe(
      (response) => {
        if (response.success) {
          const pdfBlob = this.base64ToBlob(response.fileData);
          const pdfBlobUrl = URL.createObjectURL(pdfBlob);
          this.pdfURL = pdfBlobUrl;
          this.loadPdf(pdfBlobUrl);

          // Initialize saved edits for all pages
          this.pageStates = response.savedEdits ? JSON.parse(response.savedEdits) : {};

        } else {
          console.error('Error fetching PDF:', response.error);
        }
      },
      (error) => console.error('HTTP error:', error)
    );
  }

  base64ToBlob(base64Data: string): Blob {
    const byteCharacters = atob(base64Data.split(',')[1]);
    const byteArrays = [];

    for (let offset = 0; offset < byteCharacters.length; offset += 512) {
      const slice = byteCharacters.slice(offset, offset + 512);
      const byteNumbers = new Array(slice.length);
      for (let i = 0; i < slice.length; i++) {
        byteNumbers[i] = slice.charCodeAt(i);
      }
      byteArrays.push(new Uint8Array(byteNumbers));
    }

    return new Blob(byteArrays, { type: 'application/pdf' });
  }

  async loadPdf(pdfUrl: string): Promise<void> {
    const loadingTask = pdfjsLib.getDocument({ url: pdfUrl });
    this.pdfDoc = await loadingTask.promise;
    this.totalPages = this.pdfDoc.numPages;

    this.renderPage(this.currentPage);
    this.generateThumbnails();
  }


  async renderPage(pageNumber: number): Promise<void> {
    // Save the current page's state before switching
    if (this.currentPage !== pageNumber) {
      this.saveCanvasState();
    }

    this.currentPage = pageNumber;
    const page = await this.pdfDoc.getPage(pageNumber);
    const viewport = page.getViewport({ scale: 1.5 });

    const canvas = this.pdfCanvas.nativeElement;
    const context = canvas.getContext('2d')!;
    canvas.width = viewport.width;
    canvas.height = viewport.height;

    const renderContext = {
      canvasContext: context,
      viewport,
    };

    await page.render(renderContext).promise;

    // Restore canvas state for the current page
    setTimeout(() => {
      this.restoreCanvasState();
      this.canvas.setDimensions({ width: viewport.width, height: viewport.height });
      this.canvas.renderAll();
    }, 0);
  }


  async generateThumbnails(): Promise<void> {
    const thumbnailsContainer = document.getElementById('thumbnails');
    thumbnailsContainer!.innerHTML = '';

    for (let i = 1; i <= this.totalPages; i++) {
      const page = await this.pdfDoc.getPage(i);
      const viewport = page.getViewport({ scale: 0.2 });

      const canvas = document.createElement('canvas');
      const context = canvas.getContext('2d')!;
      canvas.width = viewport.width;
      canvas.height = viewport.height;
      canvas.title = "page n°"+i;
      canvas.style.cursor= 'pointer';

      const pageWrapper = document.createElement('div');
      pageWrapper.className = 'thumbnail-wrapper';
      pageWrapper.style.display = 'flex';
      pageWrapper.style.alignItems = 'center';
      pageWrapper.style.marginBottom = '10px';
      pageWrapper.style.gap = '10px';

      const pageNumberLabel = document.createElement('div');
      pageNumberLabel.className = 'page-number';
      pageNumberLabel.textContent = `${i}`;

      pageWrapper.appendChild(canvas);
      pageWrapper.appendChild(pageNumberLabel);

      const renderContext = { canvasContext: context, viewport };
      await page.render(renderContext).promise;

      canvas.addEventListener('click', () => {
        this.currentPage = i;
        this.renderPage(i);
      });

      thumbnailsContainer!.appendChild(pageWrapper);
    }
  }

  // Function to overlay the PNG image on the PDF page and create a new downloadable PDF
  async overlayFabricEditsOnPdf(pdfUrl: string, totalPages: number): Promise<void> {
    const pdfDoc = await PDFDocument.load(await fetch(pdfUrl).then(res => res.arrayBuffer()));

    for (let pageNumber = 1; pageNumber <= totalPages; pageNumber++) {
      const page = pdfDoc.getPages()[pageNumber - 1];

      // Get the saved state for the current page
      const state = this.pageStates[pageNumber];
      if (!state) continue;

      // Create a temporary canvas to render the Fabric.js state
      const tempCanvas = document.createElement('canvas');
      tempCanvas.width = this.pdfCanvas.nativeElement.width;
      tempCanvas.height = this.pdfCanvas.nativeElement.height;
      const tempFabricCanvas = new fabric.StaticCanvas(tempCanvas);

      await new Promise<void>(resolve => {
        tempFabricCanvas.loadFromJSON(state, () => {
          tempFabricCanvas.renderAll();
          resolve();
        });
      });

      // Convert the Fabric.js canvas to an image
      const imageDataUrl = tempCanvas.toDataURL('image/png');
      const imageBytes = await fetch(imageDataUrl).then(res => res.arrayBuffer());
      const embeddedImage = await pdfDoc.embedPng(imageBytes);

      // Get PDF page dimensions and scale the image to fit
      const { width, height } = page.getSize();
      page.drawImage(embeddedImage, {
        x: 0,
        y: 0,
        width,
        height,
      });
    }

    // Save the updated PDF
    const updatedPdfBytes = await pdfDoc.save();
    const blob = new Blob([updatedPdfBytes], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);

    // Trigger download
    const link = document.createElement('a');
    link.href = url;
    link.download = 'edited.pdf';
    link.click();

    // Clean up
    URL.revokeObjectURL(url);
  }


  downloadPdfWithEdits(): void {
    // Make sure the PDF is loaded and the fabric canvas is ready
    if (this.pdfURL && this.canvas) {
      this.overlayFabricEditsOnPdf(this.pdfURL, this.totalPages)
        .then(() => {
          console.log('PDF with edits downloaded successfully.');
        })
        .catch((error) => {
          console.error('Error downloading PDF with edits:', error);
        });
    } else {
      console.error('PDF or Canvas not ready.');
    }

  }

Upvotes: 0

Views: 59

Answers (0)

Related Questions