first commit
This commit is contained in:
commit
9ca483538b
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
public/dist
|
7
README.md
Normal file
7
README.md
Normal file
@ -0,0 +1,7 @@
|
||||
InlineDocument
|
||||
===
|
||||
|
||||
## Build
|
||||
|
||||
Run command:
|
||||
`$ npm run build`
|
1565
package-lock.json
generated
Normal file
1565
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
package.json
Normal file
13
package.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"scripts": {
|
||||
"build": "webpack",
|
||||
"watch": "webpack -w"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.4.5",
|
||||
"webpack": "^5.91.0",
|
||||
"webpack-cli": "^5.1.4"
|
||||
},
|
||||
"private": true
|
||||
}
|
124
public/test.idoc
Normal file
124
public/test.idoc
Normal file
@ -0,0 +1,124 @@
|
||||
{
|
||||
"documentInfo": {
|
||||
"title": "Test",
|
||||
"author": "sakurai"
|
||||
},
|
||||
"pages": [
|
||||
{
|
||||
"pageNumber": 1,
|
||||
"layerIndex": 1,
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"drawOperators": [
|
||||
{
|
||||
"drawFunction": 2,
|
||||
"drawParameter": ["#000000"]
|
||||
},
|
||||
{
|
||||
"drawFunction": 18,
|
||||
"drawParameter": [0,0,1280,720]
|
||||
},
|
||||
{
|
||||
"drawFunction": 2,
|
||||
"drawParameter": ["rgba(255, 0, 0, 0.3)"]
|
||||
},
|
||||
{
|
||||
"drawFunction": 18,
|
||||
"drawParameter": [10,10,100,100]
|
||||
},
|
||||
{
|
||||
"drawFunction": 2,
|
||||
"drawParameter": ["#FFFFFF"]
|
||||
},
|
||||
{
|
||||
"drawFunction": 24,
|
||||
"drawParameter": ["monospace", 28, ""]
|
||||
},
|
||||
{
|
||||
"drawFunction": 22,
|
||||
"drawParameter": ["こんにちは、世界", 30, 30]
|
||||
},
|
||||
{
|
||||
"drawFunction": 22,
|
||||
"drawParameter": ["これは、InlineDocumentで文字列を出力するサンプルです。", 30, 60]
|
||||
},
|
||||
{
|
||||
"drawFunction": 4,
|
||||
"drawParameter": []
|
||||
},
|
||||
{
|
||||
"drawFunction": 14,
|
||||
"drawParameter": [100, 100]
|
||||
},
|
||||
{
|
||||
"drawFunction": 13,
|
||||
"drawParameter": [200, 200]
|
||||
},
|
||||
{
|
||||
"drawFunction": 13,
|
||||
"drawParameter": [250, 150]
|
||||
},
|
||||
{
|
||||
"drawFunction": 10,
|
||||
"drawParameter": [300, 300, 500, 500, 1600, 600]
|
||||
},
|
||||
{
|
||||
"drawFunction": 5,
|
||||
"drawParameter": []
|
||||
},
|
||||
{
|
||||
"drawFunction": 6,
|
||||
"drawParameter": []
|
||||
},
|
||||
{
|
||||
"drawFunction": 3,
|
||||
"drawParameter": ["rgba(255, 0, 0, 1)"]
|
||||
},
|
||||
{
|
||||
"drawFunction": 7,
|
||||
"drawParameter": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"pageNumber": 2,
|
||||
"layerIndex": 1,
|
||||
"width": 210,
|
||||
"height": 270,
|
||||
"drawOperators": [
|
||||
{
|
||||
"drawFunction": 17,
|
||||
"drawParameter": [0,0,210,270]
|
||||
},
|
||||
{
|
||||
"drawFunction": 2,
|
||||
"drawParameter": ["rgba(255, 0, 0, 0.3)"]
|
||||
},
|
||||
{
|
||||
"drawFunction": 18,
|
||||
"drawParameter": [10,10,100,100]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"pageNumber": 3,
|
||||
"layerIndex": 1,
|
||||
"width": 210,
|
||||
"height": 270,
|
||||
"drawOperators": [
|
||||
{
|
||||
"drawFunction": 17,
|
||||
"drawParameter": [0,0,210,270]
|
||||
},
|
||||
{
|
||||
"drawFunction": 2,
|
||||
"drawParameter": ["rgba(255, 0, 0, 0.3)"]
|
||||
},
|
||||
{
|
||||
"drawFunction": 18,
|
||||
"drawParameter": [10,10,100,100]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
79
public/viewer.css
Normal file
79
public/viewer.css
Normal file
@ -0,0 +1,79 @@
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100dvw;
|
||||
height: 100dvh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.viewer-navigation {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-bottom: 1px solid #cccccc;
|
||||
height: 50px;
|
||||
background-color: #ffffff;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.viewer-navigation-group {
|
||||
margin: 0.5em;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.viewer-navigation-group .button {
|
||||
font-size: 0.8em;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
max-width: 200px;
|
||||
border: 1px solid #aaaaaa;
|
||||
background: #eeeeee;
|
||||
padding: 5px 7px;
|
||||
}
|
||||
|
||||
.viewer-navigation-group .button:active {
|
||||
background-color: #aaaaaa;
|
||||
opacity: 0.8;
|
||||
box-shadow: 1px 1px 1px #aaaaaa;
|
||||
}
|
||||
|
||||
.viewer-navigation-group .button:hover {
|
||||
background-color: #aaaaaa;
|
||||
}
|
||||
|
||||
.viewer-container {
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.viewer {
|
||||
text-align: center;
|
||||
padding-top: 50px;
|
||||
}
|
||||
|
||||
.viewer .page {
|
||||
border: 1px solid #cccccc;
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
#openFile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#scaleOption {
|
||||
width: 100px;
|
||||
height: 25px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #dddddd;
|
||||
margin: auto;
|
||||
}
|
29
public/viewer.html
Normal file
29
public/viewer.html
Normal file
@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>InlineDocument viewer</title>
|
||||
<link rel="stylesheet" href="./viewer.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="viewer-navigation">
|
||||
<div class="viewer-navigation-group">
|
||||
<button type="button" class="button" id="openFileButton">表示</button>
|
||||
</div>
|
||||
<div class="viewer-navigation-group">
|
||||
<select id="scaleOption">
|
||||
<option value="" id="customScale">100%</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="viewer-container">
|
||||
<div id="viewer" class="viewer"></div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="file" id="openFile" />
|
||||
<script defer src="./dist/inlinedocument.js"></script>
|
||||
<script defer src="./dist/viewer.js"></script>
|
||||
</body>
|
||||
</html>
|
34
src/components/document.ts
Normal file
34
src/components/document.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { DocumentInfo } from "./document_info";
|
||||
import { PageInfo } from "./page_info";
|
||||
|
||||
class InlineDocument {
|
||||
#docInfo: DocumentInfo;
|
||||
#pages: PageInfo[];
|
||||
|
||||
constructor(
|
||||
docInfo: DocumentInfo,
|
||||
pages: PageInfo[]
|
||||
) {
|
||||
this.#docInfo = docInfo;
|
||||
this.#pages = pages;
|
||||
}
|
||||
|
||||
get documentInfo() {
|
||||
return this.#docInfo;
|
||||
}
|
||||
|
||||
get pages() {
|
||||
return this.#pages;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
documentInfo: this.#docInfo,
|
||||
pages: this.#pages,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
InlineDocument,
|
||||
}
|
31
src/components/document_info.ts
Normal file
31
src/components/document_info.ts
Normal file
@ -0,0 +1,31 @@
|
||||
class DocumentInfo {
|
||||
#title: string;
|
||||
#author: string;
|
||||
|
||||
constructor(
|
||||
title: string,
|
||||
author: string
|
||||
) {
|
||||
this.#title = title;
|
||||
this.#author = author;
|
||||
}
|
||||
|
||||
get title() {
|
||||
return this.#title;
|
||||
}
|
||||
|
||||
get author() {
|
||||
return this.#author;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
title: this.title,
|
||||
author: this.author,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
DocumentInfo,
|
||||
}
|
65
src/components/draw_operator.ts
Normal file
65
src/components/draw_operator.ts
Normal file
@ -0,0 +1,65 @@
|
||||
const DrawFunction = Object.freeze({
|
||||
/** Initialize Page for drawing. */
|
||||
Init: 0,
|
||||
transform: 1,
|
||||
fillStyle: 2,
|
||||
strokeStyle: 3,
|
||||
beginPath: 4,
|
||||
closePath: 5,
|
||||
fill: 6,
|
||||
stroke: 7,
|
||||
arc: 8,
|
||||
arcTo: 9,
|
||||
bezierCurveTo: 10,
|
||||
quadraticCurveTo: 11,
|
||||
ellipse: 12,
|
||||
lineTo: 13,
|
||||
moveTo: 14,
|
||||
rect: 15,
|
||||
roundRect: 16,
|
||||
clearRect: 17,
|
||||
fillRect: 18,
|
||||
strokeRect: 19,
|
||||
save: 20,
|
||||
restore: 21,
|
||||
fillText: 22,
|
||||
strokeText: 23,
|
||||
setFont: 24,
|
||||
rotate: 25,
|
||||
translate: 27,
|
||||
});
|
||||
|
||||
type DrawFunction = typeof DrawFunction[keyof typeof DrawFunction];
|
||||
|
||||
class DrawOperator {
|
||||
#drawFunc: DrawFunction;
|
||||
#params: any;
|
||||
|
||||
constructor(
|
||||
drawFunc: DrawFunction,
|
||||
params: any
|
||||
) {
|
||||
this.#drawFunc = drawFunc;
|
||||
this.#params = params;
|
||||
}
|
||||
|
||||
get drawFunction() {
|
||||
return this.#drawFunc;
|
||||
}
|
||||
|
||||
get params() {
|
||||
return this.#params;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
drawFunction: this.#drawFunc,
|
||||
drawParameter: this.#params,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
DrawFunction,
|
||||
DrawOperator,
|
||||
}
|
57
src/components/page_info.ts
Normal file
57
src/components/page_info.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { DrawOperator } from "./draw_operator";
|
||||
|
||||
class PageInfo {
|
||||
#pageNumber: number;
|
||||
#layerIndex: number;
|
||||
#width: number;
|
||||
#height: number;
|
||||
#operators: DrawOperator[];
|
||||
|
||||
constructor(
|
||||
pageNumber: number,
|
||||
layerIndex: number,
|
||||
width: number,
|
||||
height: number,
|
||||
operators?: DrawOperator[]
|
||||
) {
|
||||
this.#pageNumber = pageNumber;
|
||||
this.#layerIndex = layerIndex;
|
||||
this.#width = width;
|
||||
this.#height = height;
|
||||
this.#operators = operators || [];
|
||||
}
|
||||
|
||||
get pageNumber() {
|
||||
return this.#pageNumber;
|
||||
}
|
||||
|
||||
get layerIndex() {
|
||||
return this.#layerIndex;
|
||||
}
|
||||
|
||||
get width() {
|
||||
return this.#width;
|
||||
}
|
||||
|
||||
get height() {
|
||||
return this.#height;
|
||||
}
|
||||
|
||||
get operators() {
|
||||
return this.#operators;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
pageNumber: this.#pageNumber,
|
||||
layerIndex: this.#layerIndex,
|
||||
width: this.#width,
|
||||
height: this.#height,
|
||||
drawOperators: this.#operators,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
PageInfo,
|
||||
}
|
87
src/display/display.ts
Normal file
87
src/display/display.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { InlineDocument } from "../components/document";
|
||||
import { PageInfo } from "../components/page_info";
|
||||
import { CanvasFactory } from "./display_util";
|
||||
import { RendererPage } from "./renderer_page";
|
||||
|
||||
const MAX_PIXELS = 256 ** 3;
|
||||
|
||||
class Display {
|
||||
#document: InlineDocument;
|
||||
scale: number;
|
||||
|
||||
constructor(
|
||||
document: InlineDocument
|
||||
) {
|
||||
this.#document = document;
|
||||
this.scale = 1;
|
||||
}
|
||||
|
||||
get document() {
|
||||
return this.#document;
|
||||
}
|
||||
|
||||
getTotalLayerCount() {
|
||||
return this.#document.pages.length;
|
||||
}
|
||||
|
||||
getLayerCount(
|
||||
pageNum: number
|
||||
) {
|
||||
return this.#document.pages.filter(p => p.pageNumber === pageNum).length;
|
||||
}
|
||||
|
||||
async drawPage(
|
||||
pageNum: number
|
||||
) {
|
||||
const pages = this.#document.pages.filter(p => p.pageNumber === pageNum);
|
||||
if (pages.length === 0) {
|
||||
throw new RangeError(`Page not found.`);
|
||||
}
|
||||
|
||||
const canvasFactory = new CanvasFactory();
|
||||
try {
|
||||
const renderer = new RendererPage(pages[0], canvasFactory);
|
||||
|
||||
return await renderer.draw(0, 0, []);
|
||||
} finally {
|
||||
canvasFactory.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
async drawPageInfo(
|
||||
pageInfo: PageInfo
|
||||
) {
|
||||
const canvasFactory = new CanvasFactory();
|
||||
try {
|
||||
const renderer = new RendererPage(pageInfo, canvasFactory);
|
||||
|
||||
const transform = [this.scale, 0, 0, this.scale, 0, 0];
|
||||
|
||||
let width = Math.floor(pageInfo.width * this.scale);
|
||||
let height = Math.floor(pageInfo.height * this.scale);
|
||||
const pixelsInViewport = width * height;
|
||||
const maxScale = Math.sqrt(MAX_PIXELS / pixelsInViewport);
|
||||
if (1 > maxScale) {
|
||||
transform[0] = maxScale;
|
||||
transform[3] = maxScale;
|
||||
width = Math.floor(width * maxScale);
|
||||
height = Math.floor(height * maxScale);
|
||||
}
|
||||
|
||||
const bitmap = await renderer.draw(width, height, transform);
|
||||
return {
|
||||
bitmap,
|
||||
width,
|
||||
height,
|
||||
transform,
|
||||
maxScale
|
||||
};
|
||||
} finally {
|
||||
canvasFactory.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
Display,
|
||||
}
|
76
src/display/display_util.ts
Normal file
76
src/display/display_util.ts
Normal file
@ -0,0 +1,76 @@
|
||||
const isSupportOffscreenCanvas = typeof OffscreenCanvas === "function";
|
||||
|
||||
type CanvasContext = {
|
||||
canvas: OffscreenCanvas | HTMLCanvasElement,
|
||||
context: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D
|
||||
};
|
||||
|
||||
type FontStyle = "bold" | "italic" | "italic bold" | "";
|
||||
|
||||
class BaseCanvasFactory {
|
||||
static create(
|
||||
width: number,
|
||||
height: number
|
||||
): CanvasContext {
|
||||
if (isSupportOffscreenCanvas) {
|
||||
const canvas = new OffscreenCanvas(width, height);
|
||||
const context = canvas.getContext('2d');
|
||||
if (context === null) {
|
||||
throw new RangeError(`Out of memory`);
|
||||
}
|
||||
|
||||
return { canvas, context };
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const context = canvas.getContext('2d');
|
||||
if (context === null) {
|
||||
throw new RangeError(`Out of memory`);
|
||||
}
|
||||
|
||||
return { canvas, context };
|
||||
}
|
||||
}
|
||||
|
||||
class CanvasFactory {
|
||||
#canvases: Map<string, CanvasContext>;
|
||||
|
||||
constructor() {
|
||||
this.#canvases = new Map();
|
||||
}
|
||||
|
||||
getCanvas(
|
||||
id: string,
|
||||
width: number,
|
||||
height: number
|
||||
) {
|
||||
if (this.#canvases.has(id)) {
|
||||
const ctx = this.#canvases.get(id)!;
|
||||
ctx.canvas.width = width;
|
||||
ctx.canvas.height = height;
|
||||
return ctx;
|
||||
}
|
||||
|
||||
const ctx = BaseCanvasFactory.create(width, height);
|
||||
this.#canvases.set(id, ctx);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
for (const ctx of this.#canvases.values()) {
|
||||
ctx.canvas.width = 0;
|
||||
ctx.canvas.height = 0;
|
||||
}
|
||||
|
||||
this.#canvases.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
FontStyle,
|
||||
BaseCanvasFactory,
|
||||
CanvasFactory,
|
||||
CanvasContext,
|
||||
}
|
85
src/display/renderer_page.ts
Normal file
85
src/display/renderer_page.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { DrawFunction } from "../components/draw_operator";
|
||||
import { PageInfo } from "../components/page_info";
|
||||
import { CanvasContext, CanvasFactory, FontStyle } from "./display_util";
|
||||
|
||||
class RendererPage {
|
||||
#pageInfo: PageInfo;
|
||||
#canvasFactory: CanvasFactory;
|
||||
#canvas: CanvasContext | null;
|
||||
|
||||
constructor(
|
||||
pageInfo: PageInfo,
|
||||
canvasFactory: CanvasFactory,
|
||||
) {
|
||||
this.#pageInfo = pageInfo;
|
||||
this.#canvasFactory = canvasFactory;
|
||||
this.#canvas = null;
|
||||
}
|
||||
|
||||
async draw(
|
||||
width: number,
|
||||
height: number,
|
||||
transform: number[]
|
||||
) {
|
||||
if (this.#canvas === null) {
|
||||
this.#canvas = this.#canvasFactory.getCanvas(
|
||||
`p${this.#pageInfo.pageNumber}_l${this.#pageInfo.layerIndex}`,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
this.#canvas.context.setTransform(new DOMMatrix(transform));
|
||||
}
|
||||
|
||||
for (const drawOperator of this.#pageInfo.operators)
|
||||
{
|
||||
const self = this as any;
|
||||
const drawer = self[drawOperator.drawFunction] as Function | undefined;
|
||||
if (drawer === undefined) {
|
||||
const drawMethod = Object.keys(DrawFunction).find(v => (DrawFunction as any)[v] === drawOperator.drawFunction)!;
|
||||
const renderFunc = (this.#canvas.context as any)[drawMethod] as Function | undefined;
|
||||
renderFunc?.apply(this.#canvas.context, drawOperator.params);
|
||||
} else {
|
||||
await drawer.apply(this, drawOperator.params);
|
||||
}
|
||||
}
|
||||
|
||||
return await createImageBitmap(this.#canvas.canvas);
|
||||
}
|
||||
|
||||
transform(
|
||||
transform: number[]
|
||||
) {
|
||||
const [ a, b, c, d, e, f ] = transform;
|
||||
this.#canvas!.context.transform(a, b, c, d, e, f);
|
||||
}
|
||||
|
||||
fillStyle(
|
||||
fillStyle: string
|
||||
) {
|
||||
this.#canvas!.context.fillStyle = fillStyle;
|
||||
}
|
||||
|
||||
strokeStyle(
|
||||
strokeStyle: string
|
||||
) {
|
||||
this.#canvas!.context.strokeStyle = strokeStyle;
|
||||
}
|
||||
|
||||
setFont(
|
||||
fontFamily: string,
|
||||
size: number,
|
||||
fontStyle: FontStyle
|
||||
) {
|
||||
this.#canvas!.context.font = `${fontStyle} ${size}px ${fontFamily}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for (const dop of Object.keys(DrawFunction)) {
|
||||
const idop = (DrawFunction as any)[dop];
|
||||
(RendererPage.prototype as any)[idop] = (RendererPage.prototype as any)[dop];
|
||||
}
|
||||
|
||||
export {
|
||||
RendererPage,
|
||||
}
|
20
src/inlinedocument.ts
Normal file
20
src/inlinedocument.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/***
|
||||
* エントリポイントのTypescriptファイル
|
||||
*/
|
||||
|
||||
import { InlineDocument } from "./components/document";
|
||||
import { DocumentInfo } from "./components/document_info";
|
||||
import { DrawFunction, DrawOperator } from "./components/draw_operator";
|
||||
import { PageInfo } from "./components/page_info";
|
||||
import { Display } from "./display/display";
|
||||
import { DocumentReader } from "./io/document_reader";
|
||||
|
||||
(window as any).InlineDocument = {
|
||||
InlineDocument,
|
||||
DocumentInfo,
|
||||
DrawFunction,
|
||||
DrawOperator,
|
||||
PageInfo,
|
||||
Display,
|
||||
DocumentReader,
|
||||
};
|
51
src/io/document_reader.ts
Normal file
51
src/io/document_reader.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { InlineDocument } from "../components/document";
|
||||
import { DocumentInfo } from "../components/document_info";
|
||||
import { DrawFunction, DrawOperator } from "../components/draw_operator";
|
||||
import { PageInfo } from "../components/page_info";
|
||||
|
||||
class DocumentReader {
|
||||
static Parse(
|
||||
json: any
|
||||
) {
|
||||
if (!json) throw new Error(`Invalid json data.`);
|
||||
|
||||
const docInfo = this.#ParseDocumentInfo(json.documentInfo);
|
||||
const pages = [] as PageInfo[];
|
||||
for (const pageJson of json.pages) {
|
||||
pages.push(this.#ParsePages(pageJson));
|
||||
}
|
||||
|
||||
return new InlineDocument(docInfo, pages);
|
||||
}
|
||||
|
||||
static #ParseDocumentInfo(
|
||||
json: any
|
||||
) {
|
||||
if (!json) throw new Error(`Invalid json data.`);
|
||||
const title = json.title as string;
|
||||
const author = json.author as string;
|
||||
|
||||
return new DocumentInfo(title, author);
|
||||
}
|
||||
|
||||
static #ParsePages(
|
||||
json: any
|
||||
) {
|
||||
const pageNumber = json.pageNumber as number;
|
||||
const layerIndex = json.layerIndex as number;
|
||||
const width = json.width as number;
|
||||
const height = json.height as number;
|
||||
const drawOperators = [] as DrawOperator[];
|
||||
|
||||
for (const dopj of json.drawOperators) {
|
||||
const drawFunc = dopj.drawFunction as DrawFunction;
|
||||
drawOperators.push(new DrawOperator(drawFunc, dopj.drawParameter));
|
||||
}
|
||||
|
||||
return new PageInfo(pageNumber, layerIndex, width, height, drawOperators);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
DocumentReader,
|
||||
}
|
34
src/utils/promise_capability.ts
Normal file
34
src/utils/promise_capability.ts
Normal file
@ -0,0 +1,34 @@
|
||||
|
||||
class PromiseCapability<T = any> {
|
||||
promise: Promise<T>;
|
||||
resolve: (value: T) => void;
|
||||
reject: (reason?: any) => void;
|
||||
#settled: boolean;
|
||||
|
||||
constructor() {
|
||||
this.#settled = false;
|
||||
|
||||
this.resolve = null!;
|
||||
this.reject = null!;
|
||||
|
||||
this.promise = new Promise((resolve, reject) => {
|
||||
this.resolve = (value: T) => {
|
||||
this.#settled = true;
|
||||
resolve(value);
|
||||
};
|
||||
|
||||
this.reject = (reason?: any) => {
|
||||
this.#settled = true;
|
||||
reject(reason);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
get settled() {
|
||||
return this.#settled;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
PromiseCapability,
|
||||
}
|
6
src/viewer.ts
Normal file
6
src/viewer.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Viewer } from "./web/viewer";
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const viewer = new Viewer();
|
||||
viewer.run();
|
||||
});
|
71
src/web/rendering_queue.ts
Normal file
71
src/web/rendering_queue.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { Display } from "../display/display";
|
||||
import { PromiseCapability } from "../utils/promise_capability";
|
||||
import { ViewPage } from "./view_page";
|
||||
import { Viewer } from "./viewer";
|
||||
|
||||
type RenderingState = "none" | "drawing" | "drawed";
|
||||
|
||||
class RenderingQueue {
|
||||
container: HTMLDivElement;
|
||||
#viewer: Viewer;
|
||||
#pages: Map<number, ViewPage>;
|
||||
#rendering?: PromiseCapability;
|
||||
#rAF: number | null;
|
||||
|
||||
constructor(
|
||||
container: HTMLDivElement,
|
||||
viewer: Viewer
|
||||
) {
|
||||
this.container = container;
|
||||
this.#viewer = viewer;
|
||||
this.#pages = new Map();
|
||||
this.#rendering = undefined;
|
||||
this.#rAF = null;
|
||||
|
||||
this.#requestRender();
|
||||
}
|
||||
|
||||
update(
|
||||
display: Display
|
||||
) {
|
||||
for (const page of this.#pages.values()) {
|
||||
page.dispose();
|
||||
}
|
||||
|
||||
this.#pages.clear();
|
||||
|
||||
for (const page of display.document.pages) {
|
||||
const viewPage = new ViewPage(this.container, page, display);
|
||||
this.#pages.set(page.pageNumber, viewPage);
|
||||
}
|
||||
}
|
||||
|
||||
async #requestRender() {
|
||||
let tasked = false;
|
||||
for (const page of this.#pages.values())
|
||||
{
|
||||
if (page.renderingState === "none") {
|
||||
page.lastScale = page.display.scale;
|
||||
await page.draw();
|
||||
tasked = true;
|
||||
break;
|
||||
} else if (page.renderingState === "drawed" && page.lastScale !== page.display.scale) {
|
||||
page.lastScale = page.display.scale;
|
||||
page.reset();
|
||||
await page.draw();
|
||||
tasked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!tasked) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
requestAnimationFrame(this.#requestRender.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
RenderingState,
|
||||
RenderingQueue,
|
||||
}
|
87
src/web/view_page.ts
Normal file
87
src/web/view_page.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { PageInfo } from "../components/page_info";
|
||||
import { Display } from "../display/display";
|
||||
import { PromiseCapability } from "../utils/promise_capability";
|
||||
import { RenderingState } from "./rendering_queue";
|
||||
|
||||
class ViewPage {
|
||||
pageInfo: PageInfo;
|
||||
display: Display;
|
||||
div: HTMLDivElement;
|
||||
canvas?: HTMLCanvasElement;
|
||||
renderingState: RenderingState;
|
||||
lastScale: number;
|
||||
#drawPromise?: PromiseCapability;
|
||||
|
||||
constructor(
|
||||
container: HTMLDivElement,
|
||||
pageInfo: PageInfo,
|
||||
display: Display
|
||||
) {
|
||||
this.pageInfo = pageInfo;
|
||||
this.display = display;
|
||||
this.div = document.createElement('div');
|
||||
this.div.classList.add('page');
|
||||
this.renderingState = "none";
|
||||
this.lastScale = 0;
|
||||
this.#drawPromise = undefined;
|
||||
|
||||
container.append(this.div);
|
||||
}
|
||||
|
||||
reset() {
|
||||
if (this.renderingState === "drawing") {
|
||||
throw new Error(`ViewPage is currently drawing.`);
|
||||
}
|
||||
|
||||
if (this.canvas) {
|
||||
this.canvas.width = 0;
|
||||
this.canvas.height = 0;
|
||||
this.canvas.remove();
|
||||
}
|
||||
|
||||
this.renderingState = "none";
|
||||
}
|
||||
|
||||
async draw() {
|
||||
if (this.renderingState === "drawed" || this.renderingState === "drawing") {
|
||||
throw new Error(`Invalid rendering state.`);
|
||||
}
|
||||
|
||||
this.renderingState = "drawing";
|
||||
this.#drawPromise = new PromiseCapability();
|
||||
try {
|
||||
const canvas = this.canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
if (context === null) {
|
||||
throw new RangeError('Out of memory');
|
||||
}
|
||||
|
||||
const { bitmap } = await this.display.drawPageInfo(this.pageInfo);
|
||||
canvas.width = bitmap.width;
|
||||
canvas.height = bitmap.height;
|
||||
canvas.style.width = `${Math.floor(this.pageInfo.width * this.display.scale)}px`;
|
||||
canvas.style.height = `${Math.floor(this.pageInfo.height * this.display.scale)}px`;
|
||||
context.drawImage(bitmap, 0, 0);
|
||||
|
||||
this.div.style.width = `${Math.floor(this.pageInfo.width * this.display.scale)}px`;
|
||||
this.div.style.height = `${Math.floor(this.pageInfo.height * this.display.scale)}px`;
|
||||
this.div.append(canvas);
|
||||
} finally {
|
||||
this.renderingState = "drawed";
|
||||
this.#drawPromise.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
async dispose() {
|
||||
if (this.renderingState === "drawing") {
|
||||
await this.#drawPromise?.promise;
|
||||
}
|
||||
|
||||
this.reset();
|
||||
this.div.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
ViewPage,
|
||||
}
|
130
src/web/viewer.ts
Normal file
130
src/web/viewer.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import { Display } from "../display/display";
|
||||
import { DocumentReader } from "../io/document_reader";
|
||||
import { RenderingQueue } from "./rendering_queue";
|
||||
|
||||
class Viewer {
|
||||
#display?: Display;
|
||||
#renderingQueue;
|
||||
|
||||
constructor() {
|
||||
const container = document.getElementById('viewer') as HTMLDivElement;
|
||||
|
||||
this.#display = undefined;
|
||||
this.#renderingQueue = new RenderingQueue(container, this);
|
||||
}
|
||||
|
||||
get document() {
|
||||
return this.#display?.document;
|
||||
}
|
||||
|
||||
async run() {
|
||||
await this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
const openFile = document.getElementById('openFile') as HTMLInputElement;
|
||||
const openFileButton = document.getElementById('openFileButton') as HTMLButtonElement;
|
||||
|
||||
openFile.addEventListener('change', async () => {
|
||||
if (!openFile.files) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!openFile.files[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const text = await openFile.files[0].text();
|
||||
const json = JSON.parse(text);
|
||||
const idoc = DocumentReader.Parse(json);
|
||||
|
||||
this.#display = new Display(idoc);
|
||||
this.#renderingQueue.update(this.#display);
|
||||
|
||||
openFile.value = ``;
|
||||
} catch (reason) {
|
||||
console.error(reason);
|
||||
}
|
||||
});
|
||||
|
||||
openFileButton.addEventListener('click', () => {
|
||||
openFile.click();
|
||||
});
|
||||
|
||||
window.addEventListener('wheel', e => {
|
||||
if (!e.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const vectorY = e.deltaY;
|
||||
requestAnimationFrame(() => {
|
||||
if (vectorY > 0) {
|
||||
this.scaleDown(Math.abs(vectorY / 10));
|
||||
} else {
|
||||
this.scaleUp(Math.abs(vectorY / 10));
|
||||
}
|
||||
});
|
||||
}, { passive: false });
|
||||
|
||||
window.addEventListener('keydown', e => {
|
||||
if (!e.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
let rAF = null;
|
||||
switch (e.keyCode) {
|
||||
case 187:
|
||||
rAF = requestAnimationFrame(() => {
|
||||
this.scaleUp();
|
||||
});
|
||||
break;
|
||||
case 189:
|
||||
rAF = requestAnimationFrame(() => {
|
||||
this.scaleDown();
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
if (rAF) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
}, { passive: false });
|
||||
}
|
||||
|
||||
scaleDown(
|
||||
scaleFactor: number = 1
|
||||
) {
|
||||
if (this.#display) {
|
||||
this.scale(this.#display.scale - 0.01 * scaleFactor);
|
||||
}
|
||||
}
|
||||
|
||||
scaleUp(
|
||||
scaleFactor: number = 1
|
||||
) {
|
||||
if (this.#display) {
|
||||
this.scale(this.#display.scale + 0.01 * scaleFactor);
|
||||
}
|
||||
}
|
||||
|
||||
scale(
|
||||
scale: number
|
||||
) {
|
||||
if (!this.#display) {
|
||||
return;
|
||||
}
|
||||
|
||||
scale = this.#display.scale = Math.min(10, Math.max(scale, 0.1));
|
||||
|
||||
const option = document.getElementById('customScale') as HTMLOptionElement;
|
||||
option.value = option.textContent = `${(scale * 100).toFixed(2)}%`;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
Viewer,
|
||||
}
|
10
tsconfig.json
Normal file
10
tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// ソースマップを有効化
|
||||
"sourceMap": true,
|
||||
"target": "ES2015",
|
||||
"module": "ES2015",
|
||||
// 厳密モードとして設定
|
||||
"strict": true
|
||||
}
|
||||
}
|
38
webpack.config.js
Normal file
38
webpack.config.js
Normal file
@ -0,0 +1,38 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
// モード値を production に設定すると最適化された状態で、
|
||||
// development に設定するとソースマップ有効でJSファイルが出力される
|
||||
mode: 'development',
|
||||
|
||||
entry: {
|
||||
inlinedocument: './src/inlinedocument.ts',
|
||||
viewer: './src/viewer.ts',
|
||||
},
|
||||
devtool: 'source-map',
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
path: path.resolve(__dirname, 'public/dist'),
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
// 拡張子 .ts の場合
|
||||
test: /\.ts$/,
|
||||
// TypeScript をコンパイルする
|
||||
use: 'ts-loader',
|
||||
},
|
||||
],
|
||||
},
|
||||
// import 文で .ts ファイルを解決するため
|
||||
// これを定義しないと import 文で拡張子を書く必要が生まれる。
|
||||
// フロントエンドの開発では拡張子を省略することが多いので、
|
||||
// 記載したほうがトラブルに巻き込まれにくい。
|
||||
resolve: {
|
||||
// 拡張子を配列で指定
|
||||
extensions: [
|
||||
'.ts', '.js',
|
||||
],
|
||||
},
|
||||
};
|
Loading…
Reference in New Issue
Block a user