Reputation: 1
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