mirror of
https://github.com/jzillmann/pdf-to-markdown.git
synced 2024-11-21 23:33:31 +01:00
Switch from Vue to React
This commit is contained in:
parent
409e9a070f
commit
b9da57ed5b
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,3 +1,3 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
build/
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
|
25
package.json
25
package.json
@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "pdf-to-markdown",
|
"name": "pdf-to-markdown",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "A PDF to Markdown converter",
|
"description": "A PDF to Markdown Converter",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"watch": "webpack -d --watch",
|
"watch": "webpack -d --watch",
|
||||||
"build": "webpack"
|
"build": "webpack",
|
||||||
|
"lint": "eslint . --ext .js --ext .jsx --cache"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"PDF",
|
"PDF",
|
||||||
@ -13,15 +14,20 @@
|
|||||||
"Converter"
|
"Converter"
|
||||||
],
|
],
|
||||||
"author": "Johannes Zillmann",
|
"author": "Johannes Zillmann",
|
||||||
"license": "ISC",
|
"license": "Apache-2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/jzillmann/pdf-to-markdown"
|
"url": "https://github.com/jzillmann/pdf-to-markdown"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bootstrap": "^3.3.7",
|
||||||
|
"enumify": "^1.0.4",
|
||||||
"pdfjs-dist": "^1.6.317",
|
"pdfjs-dist": "^1.6.317",
|
||||||
"vue": "^2.0.5",
|
"react": "^15.3.2",
|
||||||
"vue-material": "^0.3.3"
|
"react-bootstrap": "^0.30.3",
|
||||||
|
"react-dom": "^15.3.2",
|
||||||
|
"react-dropzone": "^3.6.0",
|
||||||
|
"react-icons": "^2.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-core": "^6.18.2",
|
"babel-core": "^6.18.2",
|
||||||
@ -29,13 +35,18 @@
|
|||||||
"babel-loader": "^6.2.7",
|
"babel-loader": "^6.2.7",
|
||||||
"babel-plugin-transform-runtime": "^6.15.0",
|
"babel-plugin-transform-runtime": "^6.15.0",
|
||||||
"babel-preset-es2015": "^6.18.0",
|
"babel-preset-es2015": "^6.18.0",
|
||||||
|
"babel-preset-react": "^6.16.0",
|
||||||
"babel-preset-stage-0": "^6.16.0",
|
"babel-preset-stage-0": "^6.16.0",
|
||||||
|
"copy-webpack-plugin": "^4.0.1",
|
||||||
"css-loader": "^0.25.0",
|
"css-loader": "^0.25.0",
|
||||||
|
"esformatter-jsx": "^7.0.1",
|
||||||
|
"eslint": "^3.7.0",
|
||||||
|
"eslint-plugin-react": "^6.3.0",
|
||||||
|
"extract-text-webpack-plugin": "^1.0.1",
|
||||||
"file-loader": "^0.9.0",
|
"file-loader": "^0.9.0",
|
||||||
"html-webpack-plugin": "^2.24.1",
|
"html-webpack-plugin": "^2.24.1",
|
||||||
"sass-loader": "^4.0.2",
|
"style-loader": "^0.13.1",
|
||||||
"url-loader": "^0.5.7",
|
"url-loader": "^0.5.7",
|
||||||
"vue-loader": "^9.8.1",
|
|
||||||
"webpack": "^1.13.3"
|
"webpack": "^1.13.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
47
src/App.vue
47
src/App.vue
@ -1,47 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="app">
|
|
||||||
<hello v-if="state.uploaded"/>
|
|
||||||
<!--img src="./assets/logo.png"-->
|
|
||||||
<dropzone v-else id="myVueDropzone" url="https://httpbin.org/post" v-on:vdropzone-success="showSuccess" v-on:vdropzone-fileAdded="fileAdded"></dropzone>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import store from './store.js'
|
|
||||||
import Hello from './components/Hello'
|
|
||||||
import Dropzone from './components/Dropzone'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'app',
|
|
||||||
components: {
|
|
||||||
Hello,
|
|
||||||
Dropzone,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
state: store.state
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
'fileAdded': function (file) {
|
|
||||||
console.log('A file was added: ')
|
|
||||||
console.log(file)
|
|
||||||
},
|
|
||||||
'showSuccess': function (file) {
|
|
||||||
console.log('A file was successfully uploaded: ')
|
|
||||||
console.log(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
#app {
|
|
||||||
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
text-align: center;
|
|
||||||
color: #2c3e50;
|
|
||||||
margin-top: 60px;
|
|
||||||
}
|
|
||||||
</style>
|
|
Binary file not shown.
Before Width: | Height: | Size: 6.7 KiB |
@ -1,174 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="wrapper">
|
|
||||||
<div class="dropzone-area" @dragenter="hovering = true" @dragleave="hovering = false" :class="{hovered: hovering}">
|
|
||||||
<div class="dropzone-text">
|
|
||||||
<span class="dropzone-title">Drop image here or click to select</span>
|
|
||||||
</div>
|
|
||||||
<input type="file" @change="onFileChange">
|
|
||||||
</div>
|
|
||||||
<div class="dropzone-preview">
|
|
||||||
<img :src="image" />
|
|
||||||
<button @click="removeImage" v-if="image">Remove</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import store from '../store.js'
|
|
||||||
import pdfjs from 'pdfjs-dist';
|
|
||||||
import Page from '../models/Page.js';
|
|
||||||
import TextItem from '../models/TextItem.js';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
props : {
|
|
||||||
multiple : {
|
|
||||||
default : false
|
|
||||||
},
|
|
||||||
path : {
|
|
||||||
default : '/document/upload-unlinked/'
|
|
||||||
},
|
|
||||||
file : {
|
|
||||||
default : 'user_file'
|
|
||||||
},
|
|
||||||
files : {
|
|
||||||
default : function () { return [] }
|
|
||||||
},
|
|
||||||
target : {
|
|
||||||
default : 'dropzone'
|
|
||||||
},
|
|
||||||
clickable : {
|
|
||||||
default : false
|
|
||||||
},
|
|
||||||
previewTemplate : {
|
|
||||||
default : '<div style="display:none"></div>'
|
|
||||||
},
|
|
||||||
createImageThumbnails : {
|
|
||||||
default : false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
hovering : false,
|
|
||||||
image: null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
multipleUploads() {
|
|
||||||
return this.multiple ? true : false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onFileChange(e) {
|
|
||||||
console.debug('upload');
|
|
||||||
var files = e.target.files || e.dataTransfer.files;
|
|
||||||
console.debug(files);
|
|
||||||
if (!files.length) return;
|
|
||||||
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (evt) => {
|
|
||||||
console.debug("Loaded");
|
|
||||||
const buffer = evt.target.result;
|
|
||||||
PDFJS.getDocument(buffer).then(function (pdfDocument) {
|
|
||||||
console.log('Number of pages: ' + pdfDocument.numPages);
|
|
||||||
// console.debug(pdfDocument);
|
|
||||||
const numPages = pdfDocument.numPages;
|
|
||||||
// const numPages = 3;
|
|
||||||
store.preparePageUpload(numPages);
|
|
||||||
for (var i = 0; i <= numPages; i++) {
|
|
||||||
pdfDocument.getPage(i).then(function(page){
|
|
||||||
page.getTextContent().then(function(textContent) {
|
|
||||||
//console.debug(textContent);
|
|
||||||
const textItems = textContent.items.map(function(item) {
|
|
||||||
const transform = item.transform;
|
|
||||||
return new TextItem({
|
|
||||||
x: transform[4],
|
|
||||||
y: transform[5],
|
|
||||||
width: item.width,
|
|
||||||
height: item.height,
|
|
||||||
text: item.str
|
|
||||||
});
|
|
||||||
});
|
|
||||||
store.uploadPage(page.pageIndex, textItems);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
reader.readAsArrayBuffer(files[0]);
|
|
||||||
},
|
|
||||||
removeImage: function (e) {
|
|
||||||
this.image = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
|
||||||
<style lang='sass' scoped>
|
|
||||||
.dropzone-area {
|
|
||||||
width: 80%;
|
|
||||||
height: 200px;
|
|
||||||
position: relative;
|
|
||||||
border: 2px dashed #CBCBCB;
|
|
||||||
&.hovered {
|
|
||||||
border: 2px dashed #2E94C4;
|
|
||||||
.dropzone-title {
|
|
||||||
color: #1975A0;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropzone-area input {
|
|
||||||
position: absolute;
|
|
||||||
cursor: pointer;
|
|
||||||
top: 0px;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropzone-text {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
text-align: center;
|
|
||||||
transform: translate(0, -50%);
|
|
||||||
width: 100%;
|
|
||||||
span {
|
|
||||||
display: block;
|
|
||||||
font-family: Arial, Helvetica;
|
|
||||||
line-height: 1.9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropzone-title {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #787878;
|
|
||||||
letter-spacing: 0.4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropzone-button {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropzone-preview {
|
|
||||||
width: 80%;
|
|
||||||
position: relative;
|
|
||||||
&:hover .dropzone-button {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
img {
|
|
||||||
display: block;
|
|
||||||
height: auto;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,47 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="hello">
|
|
||||||
<h1>{{ msg }}</h1>
|
|
||||||
<div>
|
|
||||||
Uploaded: {{state.uploaded}}
|
|
||||||
</div>
|
|
||||||
<div v-for="line in state.pages">
|
|
||||||
<textarea :value="line" rows="50" cols="150"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import store from '../store.js'
|
|
||||||
export default {
|
|
||||||
name: 'hello',
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
msg: 'Welcome to Your!! Vue.js App',
|
|
||||||
state: store.state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
|
||||||
<style scoped>
|
|
||||||
h1, h2 {
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea { font-size: 18px; }
|
|
||||||
|
|
||||||
ul {
|
|
||||||
list-style-type: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #42b983;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,11 +1,12 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="UTF-8">
|
||||||
<title>PDF to Markdown</title>
|
<title>PDF to Markdown</title>
|
||||||
</head>
|
<meta name="description" content="Converts PDF files to Markdown." />
|
||||||
<body>
|
<meta name="keywords" content="PDF, Markdown, converter">
|
||||||
<div id="app"></div>
|
</head>
|
||||||
<!-- built files will be auto injected -->
|
<body>
|
||||||
</body>
|
<div id="main"</div>
|
||||||
</html>
|
</body>
|
||||||
|
</html>
|
46
src/javascript/components/App.jsx
Normal file
46
src/javascript/components/App.jsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Grid from 'react-bootstrap/lib/Grid'
|
||||||
|
|
||||||
|
import TopBar from './TopBar.jsx';
|
||||||
|
import { View } from '../models/AppState.jsx';
|
||||||
|
import PdfUploadView from './PdfUploadView.jsx';
|
||||||
|
import LoadingView from './LoadingView.jsx';
|
||||||
|
import PdfView from './PdfView.jsx';
|
||||||
|
|
||||||
|
export default class App extends React.Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
appState: React.PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
console.debug(this.props.appState);
|
||||||
|
|
||||||
|
var mainView;
|
||||||
|
switch (this.props.appState.mainView) {
|
||||||
|
case View.UPLOAD:
|
||||||
|
mainView = <PdfUploadView uploadPdfFunction={ this.props.appState.uploadPdf } />
|
||||||
|
break;
|
||||||
|
case View.LOADING:
|
||||||
|
mainView = <LoadingView/>
|
||||||
|
break;
|
||||||
|
case View.PDF_VIEW:
|
||||||
|
mainView = <PdfView pdfPages={ this.props.appState.pdfPages } />
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<TopBar/>
|
||||||
|
<Grid>
|
||||||
|
<div>
|
||||||
|
{ mainView }
|
||||||
|
</div>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
28
src/javascript/components/AppLogo.jsx
Normal file
28
src/javascript/components/AppLogo.jsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import FaFilePdfO from 'react-icons/lib/fa/file-pdf-o'
|
||||||
|
|
||||||
|
export default class AppLogo extends Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
onClick: React.PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
this.handleClick = this.handleClick.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.onClick(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<a href="" onClick={ this.handleClick }>
|
||||||
|
<FaFilePdfO/> PDF To Markdown Converter</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
22
src/javascript/components/LoadingView.jsx
Normal file
22
src/javascript/components/LoadingView.jsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Spinner from './lib/Spinner.jsx';
|
||||||
|
|
||||||
|
export default class LoadingView extends React.Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div style={ { textAlign: 'center' } }>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<Spinner/>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<div>
|
||||||
|
Uploading and parsing PDF...
|
||||||
|
</div>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
}
|
66
src/javascript/components/PdfPageView.jsx
Normal file
66
src/javascript/components/PdfPageView.jsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Table from 'react-bootstrap/lib/Table'
|
||||||
|
|
||||||
|
export default class PdfPageView extends React.Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
pdfPage: React.PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const header = "Page " + this.props.pdfPage.index;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>{ header }</h2>
|
||||||
|
<Table responsive>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
#
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Text
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
X
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Y
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Width
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Height
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{ this.props.pdfPage.textItems.map((textItem, i) => <tr key={ i }>
|
||||||
|
<td>
|
||||||
|
{ i }
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{ textItem.text }
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{ textItem.x }
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{ textItem.y }
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{ textItem.width }
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{ textItem.height }
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) }
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
47
src/javascript/components/PdfUploadView.jsx
Normal file
47
src/javascript/components/PdfUploadView.jsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Dropzone from 'react-dropzone'
|
||||||
|
import FaCloudUpload from 'react-icons/lib/fa/cloud-upload'
|
||||||
|
|
||||||
|
export default class PdfUploadView extends React.Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
uploadPdfFunction: React.PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
uploadPdfFunction: props.uploadPdfFunction,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onDrop(files) {
|
||||||
|
console.debug(files.length);
|
||||||
|
if (files.length > 1) {
|
||||||
|
alert(`Maximum one file allowed to upload, but not ${files.length}!`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const reader = new FileReader();
|
||||||
|
const uploadFunction = this.state.uploadPdfFunction;
|
||||||
|
reader.onload = (evt) => {
|
||||||
|
const fileBuffer = evt.target.result;
|
||||||
|
uploadFunction(fileBuffer);
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(files[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Dropzone onDrop={ this.onDrop.bind(this) } multiple={ false } style={ { width: 400, height: 500, borderWidth: 2, borderColor: '#666', borderStyle: 'dashed', borderRadius: 5, display: 'table-cell', textAlign: 'center', verticalAlign: 'middle' } }>
|
||||||
|
<div className="container">
|
||||||
|
<h2>Drop your PDF file here!</h2>
|
||||||
|
</div>
|
||||||
|
<h1><FaCloudUpload width={ 100 } height={ 100 } /></h1>
|
||||||
|
</Dropzone>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
25
src/javascript/components/PdfView.jsx
Normal file
25
src/javascript/components/PdfView.jsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import PdfPageView from './PdfPageView.jsx';
|
||||||
|
|
||||||
|
// A view which displays the TextItems of multiple PdfPages
|
||||||
|
export default class PdfView extends React.Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
pdfPages: React.PropTypes.array.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
console.debug(this.props.pdfPages);
|
||||||
|
const header = "Parsed " + this.props.pdfPages.length + " pages!"
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
{ header }
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
{ this.props.pdfPages.map((page) => <PdfPageView key={ page.index } pdfPage={ page } />) }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
49
src/javascript/components/TopBar.jsx
Normal file
49
src/javascript/components/TopBar.jsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Navbar from 'react-bootstrap/lib/Navbar'
|
||||||
|
import MenuItem from 'react-bootstrap/lib/MenuItem'
|
||||||
|
import Dropdown from 'react-bootstrap/lib/Dropdown'
|
||||||
|
import Popover from 'react-bootstrap/lib/Popover'
|
||||||
|
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'
|
||||||
|
|
||||||
|
import AppLogo from './AppLogo.jsx';
|
||||||
|
|
||||||
|
export default class TopBar extends React.Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
|
||||||
|
const aboutPopover = (
|
||||||
|
<Popover id="popover-trigger-click-root-close" title={ `About PDF to Markdown Converter - ${ process.env.version }` }>
|
||||||
|
<p>
|
||||||
|
<i>PDF to Markdown Converter</i> will convert your uploaded PDF to Markdown format.
|
||||||
|
</p>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Navbar inverse>
|
||||||
|
<Navbar.Header>
|
||||||
|
<Navbar.Brand>
|
||||||
|
<Dropdown id="logo-dropdown">
|
||||||
|
<AppLogo bsRole="toggle" />
|
||||||
|
<Dropdown.Menu>
|
||||||
|
<MenuItem divider />
|
||||||
|
<MenuItem href="http://github.com/jzillmann/pdf-to-markdown" target="_blank"> Github
|
||||||
|
</MenuItem>
|
||||||
|
<OverlayTrigger
|
||||||
|
trigger="click"
|
||||||
|
rootClose
|
||||||
|
placement="bottom"
|
||||||
|
overlay={ aboutPopover }>
|
||||||
|
<MenuItem eventKey="3"> About
|
||||||
|
</MenuItem>
|
||||||
|
</OverlayTrigger>
|
||||||
|
</Dropdown.Menu>
|
||||||
|
</Dropdown>
|
||||||
|
</Navbar.Brand>
|
||||||
|
</Navbar.Header>
|
||||||
|
</Navbar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
209
src/javascript/components/lib/Spinner.jsx
Normal file
209
src/javascript/components/lib/Spinner.jsx
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
// Spinner like loading indicator
|
||||||
|
export default class Spinner extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width='120px'
|
||||||
|
height='120px'
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
preserveAspectRatio="xMidYMid">
|
||||||
|
<rect
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="100"
|
||||||
|
height="100"
|
||||||
|
fill="none"></rect>
|
||||||
|
<g transform="translate(50 50)">
|
||||||
|
<g transform="rotate(0) translate(34 0)">
|
||||||
|
<circle
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="8"
|
||||||
|
fill="#000">
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
from="1"
|
||||||
|
to="0.1"
|
||||||
|
begin="0s"
|
||||||
|
dur="1s"
|
||||||
|
repeatCount="indefinite"></animate>
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
type="scale"
|
||||||
|
from="1.5"
|
||||||
|
to="1"
|
||||||
|
begin="0s"
|
||||||
|
dur="1s"
|
||||||
|
repeatCount="indefinite"></animateTransform>
|
||||||
|
</circle>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(45) translate(34 0)">
|
||||||
|
<circle
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="8"
|
||||||
|
fill="#000">
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
from="1"
|
||||||
|
to="0.1"
|
||||||
|
begin="0.12s"
|
||||||
|
dur="1s"
|
||||||
|
repeatCount="indefinite"></animate>
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
type="scale"
|
||||||
|
from="1.5"
|
||||||
|
to="1"
|
||||||
|
begin="0.12s"
|
||||||
|
dur="1s"
|
||||||
|
repeatCount="indefinite"></animateTransform>
|
||||||
|
</circle>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(90) translate(34 0)">
|
||||||
|
<circle
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="8"
|
||||||
|
fill="#000">
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
from="1"
|
||||||
|
to="0.1"
|
||||||
|
begin="0.25s"
|
||||||
|
dur="1s"
|
||||||
|
repeatCount="indefinite"></animate>
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
type="scale"
|
||||||
|
from="1.5"
|
||||||
|
to="1"
|
||||||
|
begin="0.25s"
|
||||||
|
dur="1s"
|
||||||
|
repeatCount="indefinite"></animateTransform>
|
||||||
|
</circle>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(135) translate(34 0)">
|
||||||
|
<circle
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="8"
|
||||||
|
fill="#000">
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
from="1"
|
||||||
|
to="0.1"
|
||||||
|
begin="0.37s"
|
||||||
|
dur="1s"
|
||||||
|
repeatCount="indefinite"></animate>
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
type="scale"
|
||||||
|
from="1.5"
|
||||||
|
to="1"
|
||||||
|
begin="0.37s"
|
||||||
|
dur="1s"
|
||||||
|
repeatCount="indefinite"></animateTransform>
|
||||||
|
</circle>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(180) translate(34 0)">
|
||||||
|
<circle
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="8"
|
||||||
|
fill="#000">
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
from="1"
|
||||||
|
to="0.1"
|
||||||
|
begin="0.5s"
|
||||||
|
dur="1s"
|
||||||
|
repeatCount="indefinite"></animate>
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
type="scale"
|
||||||
|
from="1.5"
|
||||||
|
to="1"
|
||||||
|
begin="0.5s"
|
||||||
|
dur="1s"
|
||||||
|
repeatCount="indefinite"></animateTransform>
|
||||||
|
</circle>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(225) translate(34 0)">
|
||||||
|
<circle
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="8"
|
||||||
|
fill="#000">
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
from="1"
|
||||||
|
to="0.1"
|
||||||
|
begin="0.62s"
|
||||||
|
dur="1s"
|
||||||
|
repeatCount="indefinite"></animate>
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
type="scale"
|
||||||
|
from="1.5"
|
||||||
|
to="1"
|
||||||
|
begin="0.62s"
|
||||||
|
dur="1s"
|
||||||
|
repeatCount="indefinite"></animateTransform>
|
||||||
|
</circle>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(270) translate(34 0)">
|
||||||
|
<circle
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="8"
|
||||||
|
fill="#000">
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
from="1"
|
||||||
|
to="0.1"
|
||||||
|
begin="0.75s"
|
||||||
|
dur="1s"
|
||||||
|
repeatCount="indefinite"></animate>
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
type="scale"
|
||||||
|
from="1.5"
|
||||||
|
to="1"
|
||||||
|
begin="0.75s"
|
||||||
|
dur="1s"
|
||||||
|
repeatCount="indefinite"></animateTransform>
|
||||||
|
</circle>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(315) translate(34 0)">
|
||||||
|
<circle
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="8"
|
||||||
|
fill="#000">
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
from="1"
|
||||||
|
to="0.1"
|
||||||
|
begin="0.87s"
|
||||||
|
dur="1s"
|
||||||
|
repeatCount="indefinite"></animate>
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
type="scale"
|
||||||
|
from="1.5"
|
||||||
|
to="1"
|
||||||
|
begin="0.87s"
|
||||||
|
dur="1s"
|
||||||
|
repeatCount="indefinite"></animateTransform>
|
||||||
|
</circle>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
32
src/javascript/functions/pdfToTextItems.jsx
Normal file
32
src/javascript/functions/pdfToTextItems.jsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import pdfjs from 'pdfjs-dist';
|
||||||
|
|
||||||
|
import AppState from '../models/AppState.jsx';
|
||||||
|
import TextItem from '../models/TextItem.jsx';
|
||||||
|
|
||||||
|
export function pdfToTextItemsAsync(fileBuffer:ArrayBuffer, appState:AppState) {
|
||||||
|
PDFJS.getDocument(fileBuffer).then(function(pdfDocument) {
|
||||||
|
console.log('Number of pages: ' + pdfDocument.numPages);
|
||||||
|
// console.debug(pdfDocument);
|
||||||
|
const numPages = pdfDocument.numPages;
|
||||||
|
// const numPages = 3;
|
||||||
|
appState.setPageCount(numPages);
|
||||||
|
for (var i = 0; i <= numPages; i++) {
|
||||||
|
pdfDocument.getPage(i).then(function(page) {
|
||||||
|
page.getTextContent().then(function(textContent) {
|
||||||
|
// console.debug(textContent);
|
||||||
|
const textItems = textContent.items.map(function(item) {
|
||||||
|
const transform = item.transform;
|
||||||
|
return new TextItem({
|
||||||
|
x: transform[4],
|
||||||
|
y: transform[5],
|
||||||
|
width: item.width,
|
||||||
|
height: item.height,
|
||||||
|
text: item.str
|
||||||
|
});
|
||||||
|
});
|
||||||
|
appState.setPdfPage(page.pageIndex, textItems);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
17
src/javascript/index.jsx
Normal file
17
src/javascript/index.jsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
|
import 'bootstrap/dist/css/bootstrap.css';
|
||||||
|
|
||||||
|
import App from './components/App.jsx';
|
||||||
|
import AppState from './models/AppState.jsx';
|
||||||
|
|
||||||
|
function render(appState) {
|
||||||
|
ReactDOM.render(<App appState={ appState } />, document.getElementById('main'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const appState = new AppState({
|
||||||
|
renderFunction: render,
|
||||||
|
});
|
||||||
|
|
||||||
|
appState.render()
|
57
src/javascript/models/AppState.jsx
Normal file
57
src/javascript/models/AppState.jsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { Enum } from 'enumify';
|
||||||
|
|
||||||
|
import { pdfToTextItemsAsync } from '../functions/pdfToTextItems.jsx'
|
||||||
|
import PdfPage from './PdfPage.jsx';
|
||||||
|
|
||||||
|
// Holds the state of the Application
|
||||||
|
export default class AppState {
|
||||||
|
|
||||||
|
constructor(options) {
|
||||||
|
this.renderFunction = options.renderFunction;
|
||||||
|
this.mainView = View.UPLOAD;
|
||||||
|
this.pagesToUpload = 0;
|
||||||
|
this.uploadedPages = 0;
|
||||||
|
this.pdfPages = [];
|
||||||
|
|
||||||
|
//bind functions
|
||||||
|
this.render = this.render.bind(this);
|
||||||
|
this.uploadPdf = this.uploadPdf.bind(this);
|
||||||
|
this.setPageCount = this.setPageCount.bind(this);
|
||||||
|
this.setPdfPage = this.setPdfPage.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.renderFunction(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadPdf(fileBuffer:ArrayBuffer) {
|
||||||
|
pdfToTextItemsAsync(fileBuffer, this);
|
||||||
|
this.mainView = View.LOADING;
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
setPageCount(numPages) {
|
||||||
|
this.pagesToUpload = numPages;
|
||||||
|
for (var i = 0; i < numPages; i++) {
|
||||||
|
this.pdfPages.push(new PdfPage({
|
||||||
|
index: i
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPdfPage(pageIndex, textItems) {
|
||||||
|
console.debug("Upload " + pageIndex);
|
||||||
|
this.pdfPages[pageIndex].textItems = textItems;
|
||||||
|
this.uploadedPages++;
|
||||||
|
if (this.uploadedPages == this.pagesToUpload) {
|
||||||
|
console.debug("Fin");
|
||||||
|
this.mainView = View.PDF_VIEW;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class View extends Enum {
|
||||||
|
}
|
||||||
|
View.initEnum(['UPLOAD', 'LOADING', 'PDF_VIEW'])
|
@ -1,4 +1,5 @@
|
|||||||
export default class Page {
|
// A page which holds TextItems displayable via PdfPageView
|
||||||
|
export default class PdfPage {
|
||||||
|
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
this.index = options.index;
|
this.index = options.index;
|
11
src/main.js
11
src/main.js
@ -1,11 +0,0 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
import App from './App'
|
|
||||||
|
|
||||||
/* eslint-disable no-new */
|
|
||||||
new Vue({
|
|
||||||
el: '#app',
|
|
||||||
template: '<App/>',
|
|
||||||
components: {
|
|
||||||
App
|
|
||||||
}
|
|
||||||
})
|
|
@ -1,15 +1,18 @@
|
|||||||
var path = require('path')
|
var path = require('path');
|
||||||
var sourceDir = path.resolve(__dirname, 'src');
|
var webpack = require('webpack');
|
||||||
|
|
||||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||||
|
|
||||||
|
var SOURCE_DIR = path.resolve(__dirname, 'src');
|
||||||
|
var BUILD_DIR = path.resolve(__dirname, 'build');
|
||||||
|
var NODEMODULES_DIR = path.resolve(__dirname, 'node_modules');
|
||||||
|
var JAVASCRIPT_DIR = SOURCE_DIR + '/javascript';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
entry: './src/main.js',
|
entry: JAVASCRIPT_DIR + '/index.jsx',
|
||||||
output: {
|
output: {
|
||||||
// To the `dist` folder
|
path: BUILD_DIR,
|
||||||
path: './dist',
|
filename: 'bundle.js'
|
||||||
// With the filename `build.js` so it's dist/build.js
|
|
||||||
filename: 'build.js'
|
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['', '.js', '.vue'],
|
extensions: ['', '.js', '.vue'],
|
||||||
@ -29,34 +32,55 @@ module.exports = {
|
|||||||
loaders: [
|
loaders: [
|
||||||
{
|
{
|
||||||
// Ask webpack to check: If this file ends with .js, then apply some transforms
|
// Ask webpack to check: If this file ends with .js, then apply some transforms
|
||||||
test: /\.js$/,
|
test: /\.jsx?$/,
|
||||||
// Transform it with babel
|
// Transform it with babel
|
||||||
loader: 'babel',
|
loader: 'babel',
|
||||||
// don't transform node_modules folder (which don't need to be compiled)
|
// don't transform node_modules folder (which don't need to be compiled)
|
||||||
exclude: /node_modules/
|
include: [JAVASCRIPT_DIR],
|
||||||
|
query: {
|
||||||
|
plugins: ['transform-runtime'],
|
||||||
|
presets: ['es2015', 'stage-0', 'react'],
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.vue$/,
|
test: /\.css$/,
|
||||||
loader: 'vue'
|
loader: "style-loader!css-loader"
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.scss$/,
|
|
||||||
loaders: ["style", "css", "sass"]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.png$/,
|
test: /\.png$/,
|
||||||
loader: "url-loader?limit=100000"
|
loader: "url-loader?limit=100000"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: /\.jpg$/,
|
||||||
|
loader: "file-loader"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/,
|
||||||
|
loader: 'url?limit=10000&mimetype=application/font-woff'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
|
||||||
|
loader: 'url?limit=10000&mimetype=application/octet-stream'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
|
||||||
|
loader: 'file'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
|
||||||
|
loader: 'url?limit=10000&mimetype=image/svg+xml'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
vue: {
|
|
||||||
loaders: {
|
|
||||||
js: 'babel'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plugins: [
|
plugins: [
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
template: sourceDir + '/index.html'
|
template: SOURCE_DIR + '/index.html'
|
||||||
})
|
}),
|
||||||
|
new CopyWebpackPlugin([
|
||||||
|
{
|
||||||
|
from: NODEMODULES_DIR + '/pdfjs-dist/build/pdf.worker.js',
|
||||||
|
to: 'bundle.worker.js'
|
||||||
|
},
|
||||||
|
])
|
||||||
]
|
]
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user