Skip to content

Commit

Permalink
Image support & drag files (#10)
Browse files Browse the repository at this point in the history
- Image support: PDFPointer now can open various images
   * It's not possible to show the thumbnails and go to the next/previous page, since only few formats support multiple images in the same container
   * HEIC images will be re-encoded using the heic2any library
- Drag files: files can now be dragged on the "Open Files" card, and they'll automatically be open
- Improved exportation: on Chromium-based browsers, if only an image must be exported the "showSaveFilePicker" function will be called instead of the "showDirectoryPicker"
  • Loading branch information
dinoosauro authored Apr 24, 2024
1 parent 5dc507b commit a1359e5
Show file tree
Hide file tree
Showing 14 changed files with 298 additions and 148 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# pdf-pointer

Display PDFs with a pointer, make quick annotations (that automatically
disappear), zoom them, and export everything as an image Try it:
Display PDFs (or images) with a pointer, make quick annotations (that
automatically disappear), zoom them, and export everything as an image Try it:
https://dinoosauro.github.io/pdf-pointer/

![An example of PDFPointer](./readme-images/example.jpg)
Expand Down Expand Up @@ -207,4 +207,5 @@ be able to use the website completely offline.

Your PDFs stays always on your device. The only external connections made by
PDFPointer are to Google Fonts' servers (and YouTube if you enable a YouTube
video for background content), but no data is shared with them.
video for background content) and to JSDelivr to download a library (heic2any)
if HEIC decoding is necessary, but no data is shared with them.
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
};
registerServiceWorker();
}
let appVersion = "2.0.4";
let appVersion = "2.1.0";
fetch("./pdfpointer-updatecode", { cache: "no-store" }).then((res) => res.text().then((text) => { if (text.replace("\n", "") !== appVersion) if (confirm(`There's a new version of pdf-pointer. Do you want to update? [${appVersion} --> ${text.replace("\n", "")}]`)) { caches.delete("pdfpointer-cache"); location.reload(true); } }).catch((e) => { console.error(e) })).catch((e) => console.error(e)); // Check if the application code is the same as the current application version and, if not, ask the user to update
</script>

Expand Down
23 changes: 22 additions & 1 deletion public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,28 @@
{
"action": "./",
"accept": {
"application/pdf": [".pdf"]
"application/pdf": [".pdf"],
"image/jpeg": [
".jpg",
".jpeg",
".jfif",
".pjpeg",
".pjp",
".heic",
".heif",
".heifs",
".heics"
],
"image/png": [".png"],
"image/gif": [".gif"],
"image/webp": [".webp"],
"image/avif": [".avif"],
"image/apng": [".apng"],
"image/bmp": [".bmp"],
"image/x-icon": [".ico", ".cur"],
"image/tiff": [".tiff", ".tif"],
"image/heic": [".heif", ".heifs", ".heic", ".heics"],
"image/heif": [".heif", ".heifs", ".heic", ".heics"]
}
}
]
Expand Down
2 changes: 1 addition & 1 deletion public/pdfpointer-updatecode
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.0.4
2.1.0
1 change: 1 addition & 0 deletions public/service-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const filestoCache = [
'./assets/index.css',
'./assets/index.js',
'./assets/path2d-polyfill.min.js',
'https://cdn.jsdelivr.net/npm/[email protected]/dist/heic2any.min.js'
];
let language = navigator.language || navigator.userLanguage;
if (language.indexOf("it") !== -1) filestoCache.push('./translationItems/it.json')
Expand Down
59 changes: 49 additions & 10 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import Header from "./Components/Header";
import Card from "./Components/Card";
import { DynamicImg } from "./Components/DynamicImg";
Expand All @@ -11,14 +11,22 @@ import { CustomProp } from "./Interfaces/CustomOptions";
import ThemeManager from "./Scripts/ThemeManager";
import Lang from "./Scripts/LanguageTranslations";
import BackgroundManager from "./Scripts/BackgroundManager";
import AlertManager from "./Scripts/AlertManager";
PDFJS.GlobalWorkerOptions.workerSrc = pdfjsWorker;
interface State {
PDFObj: PDFDocumentProxy | null,
PDFObj?: PDFDocumentProxy,
imgObj?: HTMLImageElement
hideTab?: boolean
}
declare global {
interface Window {
heic2any: (e: any) => Promise<Blob>
}
}
let installationPrompt: any;
export default function App() {
let [CurrentState, UpdateState] = useState<State>({ PDFObj: null });
let [CurrentState, UpdateState] = useState<State>({});
let cardContainer = useRef<HTMLDivElement>(null);
useEffect(() => {
let theme = JSON.parse(localStorage.getItem("PDFPointer-CurrentTheme") ?? "[]") as CustomProp["lists"];
if (theme && theme.length !== 0) ThemeManager.apply(theme);
Expand All @@ -36,24 +44,55 @@ export default function App() {
});
}, [])
async function getNewState(file: File) {
let doc = PDFJS.getDocument(await file.arrayBuffer());
document.title = `${file.name} - PDFPointer`;
let res = await doc.promise;
UpdateState(prevState => { return { ...prevState, PDFObj: res } });
if (file.type === "application/pdf") {
let doc = PDFJS.getDocument(await file.arrayBuffer());
let res = await doc.promise;
UpdateState(prevState => { return { ...prevState, PDFObj: res } });
} else {
const img = new Image();
const blob = new Blob([await file.arrayBuffer()]);
let hasTriedError = false;
img.src = URL.createObjectURL(blob);
img.onload = () => UpdateState(prevState => { return { ...prevState, imgObj: img } });
img.onerror = async () => {
const availableJpgFormats = [".jpg", ".jpeg", ".jfif", ".pjpeg", ".pjp", ".heic", ".heif", ".heifs", ".heics"] // All the extensions that might contain an HEIC image
if (availableJpgFormats.indexOf(file.name.substring(file.name.lastIndexOf("."))) !== -1 && !hasTriedError) { // Try opening HEIC image by transcoding it using the heic2any library
AlertManager.alert({ id: "HeicImage", text: "The image provided might be a HEIC image. Transcoding is being tried." })
hasTriedError = true; // Stop a possible loop, by executing this only one time
const script = document.createElement("script");
script.src = `https://cdn.jsdelivr.net/npm/[email protected]/dist/heic2any.min.js`;
script.onload = async () => {
img.src = URL.createObjectURL(await window.heic2any({ blob }));
}
document.body.append(script);
}
}
}
}
return <>
<Header></Header><br></br>
{CurrentState.PDFObj === null ? <>
<div className={!CurrentState.hideTab && !window.matchMedia('(display-mode: standalone)').matches ? "doubleFlex" : undefined}>
{!CurrentState.PDFObj && !CurrentState.imgObj ? <>
<div onDragOver={(e) => e.preventDefault()} ref={cardContainer} onDragEnter={() => cardContainer.current?.classList?.add("drag")} onDragLeave={() => cardContainer.current?.classList?.remove("drag")} onDrop={async (e) => { // Get the dropped file, and open it
e.preventDefault();
cardContainer.current?.classList?.remove("drag");
if (e.dataTransfer.items) {
if (e.dataTransfer.items[0].kind === "file") {
const file = e.dataTransfer.items[0].getAsFile();
file && getNewState(file);
}
} else getNewState(e.dataTransfer.files[0]);
}} className={!CurrentState.hideTab && !window.matchMedia('(display-mode: standalone)').matches ? "doubleFlex" : undefined}>
<Card>
<h2>{Lang("Choose file")}</h2>
<div className="center" style={{ width: "100%" }}>
<DynamicImg id="laptop" width={200}></DynamicImg><br></br>
</div>
<i>{Lang("Don't worry. Everything will stay on your device.")}</i><br></br><br></br>
<i>{`${Lang("Don't worry. Everything will stay on your device.")} ${Lang("You can also drop files here.")} ${window.matchMedia('(display-mode: standalone)').matches ? Lang("Moreover, you can also open the files from the native file picker (Open With -> PDFPointer)") : ""}`}</i><br></br><br></br>
<button onClick={() => { // Get the PDF file
let input = document.createElement("input");
input.type = "file";
input.accept = "application/pdf, image/*"
input.onchange = () => {
input.files !== null && getNewState(input.files[0]);
}
Expand All @@ -76,7 +115,7 @@ export default function App() {
</div>
</> : <>
<Card>
<PdfObj pdfObj={CurrentState.PDFObj}></PdfObj>
<PdfObj pdfObj={CurrentState.PDFObj} imgObj={CurrentState.imgObj}></PdfObj>
</Card>
</>
}
Expand Down
2 changes: 1 addition & 1 deletion src/Components/CircularButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export default function CircularButton({ imgId, click, marginLeft, marginRight,
}}>
<DynamicImg id={`${imgId}${enabled && isSelectable ? "_fill" : ""}`} width={32}></DynamicImg>
</div>
{hint && <div className="dropdownOpen opacity opacityHover" style={{ position: "fixed", left: 0 }} ref={hintDiv}>
{hint && <div className="dropdownOpen opacity opacityHover noEvents" style={{ position: "fixed", left: 0 }} ref={hintDiv}>
<label>{hint}</label>
</div>}
</div>
Expand Down
25 changes: 14 additions & 11 deletions src/Components/ExportDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@ let exportValue = {
}

interface Props {
pdfObj: PDFJS.PDFDocumentProxy
pdfObj?: PDFJS.PDFDocumentProxy,
imgObj?: HTMLImageElement
}
/**
* Export the PDF as a group of images. This ReactNode contains the options for that exportation process.
* @param pdfObj the PDF object that'll be used for exporting things
* @param imgObj the image where the annotations (or filters) will be drawn
* @returns the Export dialog ReactNode
*/
export default function ExportDialog({ pdfObj }: Props) {
export default function ExportDialog({ pdfObj, imgObj }: Props) {
let exportButton = useRef<HTMLButtonElement>(null);

return <div>
Expand All @@ -33,12 +35,13 @@ export default function ExportDialog({ pdfObj }: Props) {
<option value={"png"}>PNG</option>
{document.createElement("canvas").toDataURL("image/webp").startsWith("data:image/webp") && <option value={"webp"}>WEBP</option>}
</select> {Lang("format")}</label><br></br><br></br>
<label>{Lang("Write the number of pages to export:")}</label><br></br>
<i style={{ fontSize: "0.75em" }}>{Lang(`Separate pages with a comma, or add multiple pages with a dash: "1-5,7"`)}</i><br></br>
<input style={{ marginTop: "10px" }} type="text" defaultValue={exportValue.pages} onInput={(e) => {
exportValue.pages = (e.target as HTMLInputElement).value;
if (exportButton.current) exportButton.current.disabled = !/^[0-9,-]*$/.test(exportValue.pages);
}}></input><br></br><br></br>
{pdfObj && <>
<label>{Lang("Write the number of pages to export:")}</label><br></br>
<i style={{ fontSize: "0.75em" }}>{Lang(`Separate pages with a comma, or add multiple pages with a dash: "1-5,7"`)}</i><br></br>
<input style={{ marginTop: "10px" }} type="text" defaultValue={exportValue.pages} onInput={(e) => {
exportValue.pages = (e.target as HTMLInputElement).value;
if (exportButton.current) exportButton.current.disabled = !/^[0-9,-]*$/.test(exportValue.pages);
}}></input><br></br><br></br></>}
<label>{Lang("Choose the size of the output image:")}</label><br></br>
<input type="range" min={0.5} max={8} step={0.01} defaultValue={exportValue.scale} onChange={(e) => { exportValue.scale = parseFloat((e.target as HTMLInputElement).value) }}></input><br></br><br></br>
<label>{Lang("Choose the quality of the output image:")}</label><br></br>
Expand All @@ -56,7 +59,7 @@ export default function ExportDialog({ pdfObj }: Props) {
<button ref={exportButton} onClick={async () => {
let handle;
try { // If the user doesn't want to save the file as ZIP, and the File System API is supported, use the "showDirectoryPicker" method
handle = window.showDirectoryPicker !== undefined && !exportValue.zip ? await window.showDirectoryPicker({ mode: "readwrite", id: "PDFPointer-PDFExportFolder" }) : undefined;
handle = (exportValue.pages.indexOf("-") !== -1 || exportValue.pages.indexOf(",") !== -1) ? window.showDirectoryPicker !== undefined && !exportValue.zip ? await window.showDirectoryPicker({ mode: "readwrite", id: "PDFPointer-PDFExportFolder" }) : undefined : window.showSaveFilePicker !== undefined && !exportValue.zip ? await window.showSaveFilePicker({ id: "PDFPointer-PDFExportFolder", types: [{ "description": "The selected image", accept: { [`image/${exportValue.img}`]: [`.${exportValue.img === "jpeg" ? "jpg" : exportValue.img}`] } }] }) : undefined; // If there are no "," or "-" the item to download is only an image, therefore try to use the File System API for a single file. Otherwise, try to use the File System API for folders
} catch (ex) {
console.warn({
type: "RejectedPicker",
Expand All @@ -65,7 +68,7 @@ export default function ExportDialog({ pdfObj }: Props) {
ex: ex
})
}
ImageExport({ imgType: exportValue.img, pages: exportValue.pages, getAnnotations: exportValue.annotations, pdfObj: pdfObj, scale: exportValue.scale, useZip: exportValue.zip, quality: exportValue.quality, handle: handle, filter: exportValue.filter })
}}>{Lang("Export images")}</button>
ImageExport({ imgType: exportValue.img, pages: exportValue.pages, getAnnotations: exportValue.annotations, pdfObj: pdfObj, scale: exportValue.scale, useZip: exportValue.zip, quality: exportValue.quality, handle: handle, filter: exportValue.filter, imgObj: imgObj })
}}>{Lang("Export image(s)")}</button>
</div>
}
Loading

0 comments on commit a1359e5

Please sign in to comment.