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
- Uygulamanız henüz kullanmıyorsa herhangi bir sunucu kurmanıza ve bakımını yapmanıza gerek yoktur.
- Sunucu (lar) ınız daha az kaynak kullanacak çünkü tüm işi tarayıcı yapıyor.
- Bu, araçlarınıza bağlı olsa da, mevcut tüm ön uç stil çerçevenizden yararlanabilirsiniz.
2. Hangi JS kitaplığı kullanmalıyız?
Ön uçta PDF belgeleri oluşturmak için kullanabileceğiniz 3 popüler JavaScript kitaplığı vardır.
Biz Neden jsPDF'yi Tercih Ettik
Bu proje için şu nedenlerle jsPDF'yi seçtik :
- 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
Basit tutmak için, uygulamayı Create React App ile kuracağız . Projeyi başlatmak için terminalinizde tek bir komut çalıştırın:
npx create-react-app pdf-from-images-react-app --template typescript
Kurulum tamamlandığında jspdf
paketi ekleyelim:
npm install jspdf
Ardından React uygulamasını şu şekilde başlatın:
npm start
UI iskeleti
İşlevselliği uygulamadan önce, uygulama kullanıcı arayüzünü hazırlayacağız.
Tüm içeriğini App.tsx
aşağıdaki kodla değiştirebilirsiniz:
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.tsx
ve 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
Bir PDF oluşturmadan önce, görüntüleri cihazınızdan okumanız gerekir. App.tsx
zaten dosya input
seçmek için bir dosya öğesi içeriyor , şimdi seçili görüntüleri uygulama durumuna getirmemiz gerekiyor.
handleImageUpload
Yüklenen görüntüleri işleyen ve durumu güncelleyen işleve dikkat edin :
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
Son adım, nihayet tüm görüntüleri içeren bir PDF belgesi oluşturmaktır. App.tsx
Aşağıdaki değişikliklerle güncellemenizi yaptıktan sonra , React uygulamanız tamamlanır.
Yeni handleGeneratePdfFromImages
fonksiyonu, PDF oluşturma ve uygulama temizleme ile ilgilenir. Artık "Generate PDF" düğmesini tıkladığınızda, PDF'niz indirilmeye hazırdır.
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.