PDFをGyazoにあげてScrapboxに取り込むUserScript
2022-12-01 16:26:41 deprecated
/icons/hr.icon
2021-01-22 17:16:19 後ほど、要素要素に分解するtakker.icon
moduleに分解して、使いやすくする
/icons/done.icon隠し<input>を作ってfileを取得する
Promiseで包むと使いやすそう
/icons/done.icon画像をGyazoに上げる
認証なしと認証ありとの2パターンを使いたい
認証なしで取得したURLをfetchすれば永続化できるので、認証ありは必要ない
個人的には、PDF1ページにScrapbox1ページを分けたい
進捗状況を出せるように特化させてもいいかも
自前で一度にfetchする数を制限すればいいだけ
18:27:16 実装した
なんでだろう?
ただの画像だし
code:script.js
/* MIT License Copyright (c) 2020 ci7lus */
import {installCDN} from '/api/code/takker/scrapbox-install-CDN/script.js';
import {insertText} from '/api/code/takker/scrapbox-insert-text/script.js';
import '/api/code/takker/PDFをGyazoにあげてScrapboxに取り込むUserScript/progressBar.js';
import {getLocalFiles} from '/api/code/takker/簡単にfileをbrowserに取り込むscript/script.js';
(async () => {
await installCDN({
id: 'pdfMinJs-for-scrapbox',
src: '//cdnjs.cloudflare.com/ajax/libs/pdf.js/2.2.228/pdf.min.js'});
await installCDN({
id: 'pdfWorkerMinJs-for-scrapbox',
src: '//cdnjs.cloudflare.com/ajax/libs/pdf.js/2.2.228/pdf.worker.min.js'});
scrapbox.PageMenu.addMenu({
title: 'upload-pdf',
onClick: async () => {
const file = await getLocalFiles({accept: 'application/pdf,.pdf'});
if (!file) return;
const filename = file.name;
console.log(file);
// 進捗表示エリア
const progressArea = document.createElement('progress-area');
document.body.appendChild(progressArea);
try {
// cors制限のためcmapsはstorage.googleapis.comに上げたものを用いる
progressArea.message = 'Loading pdf file...';
const pdf = await pdfjsLib.getDocument({
data: await toArrayBuffer(file),
ここ本当は自分で上げたcmapsを使わないとだめそう
他人のを勝手に使うのはよくない
scrapbox.ioにuploadしたやつなら使えるのかな?
directoryでuploadする必要があるから無理そう
cmapsは必要っぽい?
storage.google.apiを使用しないでcmapsを使う方法は、これがヒントになりそう
同じドメインにdirectoryをuploadしているだけ
code:script.js
cMapUrl:
cMapPacked: true,
}).promise;
const metadata = await pdf.getMetadata();
console.log(metadata);
const isBigImage = window.pdfscrap_big ?? true;
progressArea.message = 'pdf file loaded. Uploading to gyazo...';
let progress = 0;
const updateProgressText = () => {
progressArea.message = `Uploading... (${progress++}/${
pdf.numPages
})`;
};
const uploadPage = async (page) => {
const dataURI = await pdf2imageDataURL(await pdf.getPage(page));
const {get_image_url} = await uploadToGyazoEasily({
dataURI, title: file.name,
clientId: 'fd84cef882b9b51d8de3365cd28c86bc2cc8a1646ef05dbc641ca49278f9d0d6',});
前ページの画像を取得するまで待つ
これ必要なのかな?
code:script.js
while (true) {
// 登録順序を保証する
if (page - 1 <= progress) break;
await sleep(100);
}
画像URLを永続化する
code:script.js
const getImage = await fetch(get_image_url, {
mode: 'cors',
credentials: 'include',
});
updateProgressText();
console.log(page${page} -> ${getImage.url});
return getImage.url;
};
const pages = await throttle(
.map(i => i + 1)
.map(page => uploadPage(page)),
{maxInProgress: 5},
);
progressArea.message = 'done';
const urls = pages.map((url) => isBigImage ? [[${url}]] : [${url}]);
urls.unshift(file.name);
insertText({text: urls.join('\n') + '\n'});
} catch (e) {
console.log('failed', file, e);
alert(failed to load: ${filename});
} finally {
document.body.removeChild(progressArea);
}
},
});
})();
code:script.js
const toArrayBuffer = (blob) => blob.arrayBuffer?.() ?? new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = reject;
reader.onload = () => {
resolve(reader.result);
};
reader.readAsArrayBuffer(blob);
});
PDFの1ページを画像に変換する
code:script.js
async function pdf2imageDataURL(pdfPage, {type = 'image/png'} = {}) {
const viewport = pdfPage.getViewport({
scale: window.devicePixelRatio ?? 1.5,
});
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const renderContext = {canvasContext: ctx, viewport: viewport};
canvas.height = viewport.height;
canvas.width = viewport.width;
await pdfPage.render(renderContext).promise;
return canvas.toDataURL(type);
}
待つ
code:script.js
const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds));
この後なるべく早くget_image_urlをfetchする必要がある
code:script.js
async function uploadToGyazoEasily({dataURI, clientId, title}) {
const formData = new FormData();
formData.append('image_url', dataURI);
formData.append('client_id', clientId);
formData.append('referer_url', location.href);
formData.append('title', title);
const easyAuth = await fetch(
{
method: 'POST',
mode: 'cors',
credentials: 'include',
body: formData,
},
);
return await easyAuth.json();
}
一定数だけ同時にPromiseを実行する
code:script.js
async function throttle(promises, {maxInProgress = undefined} = {}) {
if (!maxInProgress || maxInProgress < 0) return await Promise.all(promises);
let progressList = promises.map(_ => false);
const promiseList = promises.map((promise, i) => {return {index: i, promise};});
const result = [];
.map(async index => {
do {
progressListindex = true; result.push(await promisesindex); index = progressList.findIndex(state => !state);
} while(index !== -1)
}));
return result;
}
Progress bar
Progress barというより、ただのmessage windowだな
code:progressBar.js
customElements.define('progress-area', class extends HTMLElement {
connectedCallback() {
if (this.rendered) return;
this.render();
this.rendered = true;
}
render() {
const shadow = this.shadowRoot ?? this.attachShadow({mode: 'open'});
shadow.innerHTML = `
<style>
div {
position: fixed;
top: 0; right: 0;
margin: 1rem; padding: 1rem;
background: #FFF; color: 000; z-index: 999999;
}
</style>
<div>
${this.getAttribute('message')}
</div>`;
}
get message() {
this.getAttribute('message');
}
set message(message) {
this.setAttribute('message', message);
}
static get observedAttributes() {
}
attributeChangedCallback(name, oldValue, newValue) {
this.render();
}
});