Mali tablolar, hesap bilgileri veya yalnızca tek bir dosyada paylaşılması gereken önemli bilgiler gibi çeşitli nedenlerle web uygulamanızın kullanıcılarına bir PDF belgesi sağlamanız gerekebilir.

Bu makalede, neden istemci tarafında PDF oluşturmayı düşünmeniz gerektiğini ve bu işlevi web'inize veya daha spesifik olarak React uygulamasına nasıl ekleyebileceğinizi açıklayacağım.

Baştan itibaren neler olup bittiğine dair iyi bir genel bakışa sahip olmaktan hoşlandığım için, devam etmeden önce uygulamanın canlı demosunu ve kaynak kodunu görebilirsiniz:

Canlı demo: https://pixochi.github.io/pdf-from-images-react-app/
Kaynak kodu: https://github.com/pixochi/pdf-from-images-react-app

1. Neden İstemci Tarafında / FrontEnd

2. Hangi JS kitaplığı kullanmalıyız?

Biz Neden jsPDF'yi Tercih Ettik

  • En yüksek GitHub yıldızı sayısı (20.3K)
  • Vade (2014'te piyasaya sürüldü) yani kararlı bir sürüm
  • En küçük boyut (PDFKit'ten% 87 daha küçük ve pdfmake'den% 460 daha küçük)
  • Local TypeScript desteği
  • Anlaşılır Döküman Desteği

3. React uygulamasını oluşturalım

İlk kurulum

npx create-react-app pdf-from-images-react-app --template typescript

Kurulum tamamlandığında jspdf paketi ekleyelim:

npm install jspdf
npm start

UI iskeleti

import React from "react";

import "./App.css";

// Placeholder for future app functionality,
// the actual functionality will be implemented later.
const NO_OP = () => {};

function App() {
  // State for uploaded images
  const [uploadedImages, setUploadedImages] = React.useState<any>([]);

  return (
    <>
      <h1>Convert images to PDFs</h1>

      {/* Overview of uploaded images */}
      <div className="images-container">
        {uploadedImages.length > 0 ? (
          uploadedImages.map((image: any) => (
            <img key={image.name} src={image.src} className="uploaded-image" />
          ))
        ) : (
          <p>Upload some images...</p>
        )}
      </div>

      {/* Buttons for uploading images and generating a PDF */}
      <div className="buttons-container">
        {/* Uploads images */}
        <label htmlFor="file-input">
          <span className="button">Upload images</span>
          <input
            id="file-input"
            type="file"
            accept="image/*"
            onChange={NO_OP}
            // Native file input is hidden only for styling purposes
            style={{ display: "none" }}
            multiple
          />
        </label>

        {/* Generates PDF */}
        <button
          onClick={NO_OP}
          className="button"
          disabled={uploadedImages.length === 0}
        >
          Generate PDF
        </button>
      </div>
    </>
  );
}

export default App;

Yukarıdaki kodda, birkaç className s fark edebilirsiniz - bunlar, uygulamanın minimum stil ayarlamaları içindir. Aşağıdaki CSS kurallarını içeren tüm ' className kaynaklıdır; App.css dosyamınızı açıyoruz ve aşağıdaki kodları ekliyoruz. 

body {
  margin: 20px;
}

.images-container {
    display: flex;
    box-sizing: border-box;
    width: 100%;
    height: 500px;
    max-height: 30vh;
    padding: 16px 20px;
    overflow: auto;
    background: #efefef;
    border-radius: 6px;
    box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
}

.images-container:after {
  content: '';
  padding-right: 20px;
}

.uploaded-image {
  height: 100%;
  width: auto;
  box-shadow: 0 3px 6px rgba(0,0,0,0.26), 0 3px 6px rgba(0,0,0,0.3);
}

.uploaded-image:not(:first-child) {
  margin-left: 20px;
}

.buttons-container {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  margin-top: 20px;
}

.button {
  display: inline-block;
  background-color: #4CAF50;
  color: white;
  font-size: 18px;
  font-weight: 600;
  text-align: center;
  text-decoration: none;
  padding: 16px 32px;
  border: none;
  border-radius: 3px;
  box-sizing: border-box;
  box-shadow: 0 3px 6px rgba(0,0,0,0.3)
}

.button:focus {
  outline: none;
}

.button:active {
  filter: brightness(0.9);
}

.button:disabled {
  background-color: #ababab;
}

.button:not(:disabled) {
  cursor: pointer;
}

.button:not(:first-child) {
  margin-left: 16px;
}

İçerik yerini ile App.tsxve App.css, uygulamamızda başarılı bir şekilde uygulandığından emin olmak için uygulama tarayıcınızda bu göz atmalısınız.

Cihazınızdan resim yükleme

import React, { ChangeEventHandler } from "react";

import "./App.css";

// Placeholder for future app functionality,
// the actual functionality will be implemented later.
const NO_OP = () => {};

// New class with additional fields for Image
class CustomImage extends Image {
  constructor(public mimeType: string) {
    super();
  }

  // `imageType` is a required input for generating a PDF for an image.
  get imageType(): string {
    return this.mimeType.split("/")[1];
  }
}

// Each image is loaded and an object URL is created.
const fileToImageURL = (file: File): Promise<CustomImage> => {
  return new Promise((resolve, reject) => {
    const image = new CustomImage(file.type);

    image.onload = () => {
      resolve(image);
    };

    image.onerror = () => {
      reject(new Error("Failed to convert File to Image"));
    };

    image.src = URL.createObjectURL(file);
  });
};

function App() {
  // State for uploaded images
  const [uploadedImages, setUploadedImages] = React.useState<CustomImage[]>([]);

  const handleImageUpload = React.useCallback<
    ChangeEventHandler<HTMLInputElement>
  >(
    (event) => {
      // `event.target.files` is of type `FileList`,
      // we convert it to Array for easier manipulation.
      const fileList = event.target.files;
      const fileArray = fileList ? Array.from(fileList) : [];

      // Uploaded images are read and the app state is updated.
      const fileToImagePromises = fileArray.map(fileToImageURL);
      Promise.all(fileToImagePromises).then(setUploadedImages);
    },
    [setUploadedImages]
  );

  return (
    <>
      <h1>Convert images to PDFs</h1>

      {/* Overview of uploaded images */}
      <div className="images-container">
        {uploadedImages.length > 0 ? (
          uploadedImages.map((image) => (
            <img key={image.src} src={image.src} className="uploaded-image" />
          ))
        ) : (
          <p>Upload some images...</p>
        )}
      </div>

      {/* Buttons for uploading images and generating a PDF */}
      <div className="buttons-container">
        {/* Uploads images */}
        <label htmlFor="file-input">
          <span className="button">Upload images</span>
          <input
            id="file-input"
            type="file"
            accept="image/*"
            onChange={handleImageUpload}
            // Native file input is hidden only for styling purposes
            style={{ display: "none" }}
            multiple
          />
        </label>

        {/* Generates PDF */}
        <button
          onClick={NO_OP}
          className="button"
          disabled={uploadedImages.length === 0}
        >
          Generate PDF
        </button>
      </div>
    </>
  );
}

export default App;

En son değişikliklerle, yeşil “Upload Images” düğmesine tıklayabilmeli, resimleri seçebilmeli ve ardından resimler kullanıcı arayüzünde görüntülenmelidir.

Yüklenen görüntülerden bir PDF oluşturma

import React, { ChangeEventHandler } from "react";
import jsPDF from "jspdf";

import "./App.css";

// New class with additional fields for Image
class CustomImage extends Image {
  constructor(public mimeType: string) {
    super();
  }

  // `imageType` is a required input for generating a PDF for an image.
  get imageType(): string {
    return this.mimeType.split("/")[1];
  }
}

// Each image is loaded and an object URL is created.
const fileToImageURL = (file: File): Promise<CustomImage> => {
  return new Promise((resolve, reject) => {
    const image = new CustomImage(file.type);

    image.onload = () => {
      resolve(image);
    };

    image.onerror = () => {
      reject(new Error("Failed to convert File to Image"));
    };

    image.src = URL.createObjectURL(file);
  });
};

// The dimensions are in millimeters.
const A4_PAPER_DIMENSIONS = {
  width: 210,
  height: 297,
};

const A4_PAPER_RATIO = A4_PAPER_DIMENSIONS.width / A4_PAPER_DIMENSIONS.height;

interface ImageDimension {
  width: number;
  height: number;
}

// Calculates the best possible position of an image on the A4 paper format,
// so that the maximal area of A4 is used and the image ratio is preserved.
const imageDimensionsOnA4 = (dimensions: ImageDimension) => {
  const isLandscapeImage = dimensions.width >= dimensions.height;

  // If the image is in landscape, the full width of A4 is used.
  if (isLandscapeImage) {
    return {
      width: A4_PAPER_DIMENSIONS.width,
      height:
        A4_PAPER_DIMENSIONS.width / (dimensions.width / dimensions.height),
    };
  }

  // If the image is in portrait and the full height of A4 would skew
  // the image ratio, we scale the image dimensions.
  const imageRatio = dimensions.width / dimensions.height;
  if (imageRatio > A4_PAPER_RATIO) {
    const imageScaleFactor =
      (A4_PAPER_RATIO * dimensions.height) / dimensions.width;

    const scaledImageHeight = A4_PAPER_DIMENSIONS.height * imageScaleFactor;

    return {
      height: scaledImageHeight,
      width: scaledImageHeight * imageRatio,
    };
  }

  // The full height of A4 can be used without skewing the image ratio.
  return {
    width: A4_PAPER_DIMENSIONS.height / (dimensions.height / dimensions.width),
    height: A4_PAPER_DIMENSIONS.height,
  };
};

// Creates a PDF document containing all the uploaded images.
const generatePdfFromImages = (images: CustomImage[]) => {
  // Default export is A4 paper, portrait, using millimeters for units.
  const doc = new jsPDF();

  // We let the images to add all pages,
  // therefore the first default page can be removed.
  doc.deletePage(1);

  images.forEach((image) => {
    const imageDimensions = imageDimensionsOnA4({
      width: image.width,
      height: image.height,
    });

    doc.addPage();
    doc.addImage(
      image.src,
      image.imageType,
      // Images are vertically and horizontally centered on the page.
      (A4_PAPER_DIMENSIONS.width - imageDimensions.width) / 2,
      (A4_PAPER_DIMENSIONS.height - imageDimensions.height) / 2,
      imageDimensions.width,
      imageDimensions.height
    );
  });

  // Creates a PDF and opens it in a new browser tab.
  const pdfURL = doc.output("bloburl");
  window.open(pdfURL as any, "_blank");
};

function App() {
  // State for uploaded images
  const [uploadedImages, setUploadedImages] = React.useState<CustomImage[]>([]);

  const handleImageUpload = React.useCallback<
    ChangeEventHandler<HTMLInputElement>
  >(
    (event) => {
      // `event.target.files` is of type `FileList`,
      // we convert it to Array for easier manipulation.
      const fileList = event.target.files;
      const fileArray = fileList ? Array.from(fileList) : [];

      // Uploaded images are read and the app state is updated.
      const fileToImagePromises = fileArray.map(fileToImageURL);
      Promise.all(fileToImagePromises).then(setUploadedImages);
    },
    [setUploadedImages]
  );

  const cleanUpUploadedImages = React.useCallback(() => {
    setUploadedImages([]);
    uploadedImages.forEach((image) => {
      // The URL.revokeObjectURL() releases an existing object URL
      // which was previously created by URL.createObjectURL().
      // It lets the browser know not to keep the reference to the file any longer.
      URL.revokeObjectURL(image.src);
    });
  }, [setUploadedImages, uploadedImages]);

  const handleGeneratePdfFromImages = React.useCallback(() => {
    generatePdfFromImages(uploadedImages);
    cleanUpUploadedImages();
  }, [uploadedImages, cleanUpUploadedImages]);

  return (
    <>
      <h1>Convert images to PDFs</h1>

      {/* Overview of uploaded images */}
      <div className="images-container">
        {uploadedImages.length > 0 ? (
          uploadedImages.map((image) => (
            <img key={image.src} src={image.src} className="uploaded-image" />
          ))
        ) : (
          <p>Upload some images...</p>
        )}
      </div>

      {/* Buttons for uploading images and generating a PDF */}
      <div className="buttons-container">
        {/* Uploads images */}
        <label htmlFor="file-input">
          <span className="button">Upload images</span>
          <input
            id="file-input"
            type="file"
            accept="image/*"
            onChange={handleImageUpload}
            // Native file input is hidden only for styling purposes
            style={{ display: "none" }}
            multiple
          />
        </label>

        {/* Generates PDF */}
        <button
          onClick={handleGeneratePdfFromImages}
          className="button"
          disabled={uploadedImages.length === 0}
        >
          Generate PDF
        </button>
      </div>
    </>
  );
}

export default App;

Orjinal Makale : https://medium.com/javascript-in-plain-english/generating-pdf-from-images-on-the-client-side-with-react-a971b61de28c

Arkadaşlar umarım yardımcı olmuştur.