first commit

This commit is contained in:
Sakurai Ryota 2024-12-10 20:47:34 +09:00
commit 9ab627d5c1
21 changed files with 4657 additions and 0 deletions

BIN
.gitignore vendored Normal file

Binary file not shown.

BIN
src/.gitignore vendored Normal file

Binary file not shown.

186
src/dist/bundle.js vendored Normal file
View File

@ -0,0 +1,186 @@
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({
/***/ "./doc_viewer/api/canvas_factory.js":
/*!******************************************!*\
!*** ./doc_viewer/api/canvas_factory.js ***!
\******************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ CanvasFactory: () => (/* binding */ CanvasFactory)\n/* harmony export */ });\n\r\nclass CanvasFactory {\r\n /**\r\n * @param {number} width\r\n * @param {number} height\r\n */\r\n create(width, height) {\r\n if (typeof OffscreenCanvas !== \"undefined\") {\r\n const canvas = new OffscreenCanvas(width, height);\r\n const context = canvas.getContext('2d');\r\n return {\r\n canvas,\r\n context,\r\n };\r\n } else if (typeof document !== \"undefined\") {\r\n const canvas = document.createElement('canvas');\r\n canvas.width = width;\r\n canvas.height = height;\r\n const context = canvas.getContext('2d');\r\n return {\r\n canvas,\r\n context,\r\n };\r\n } else {\r\n throw new Error(`Not implemented`);\r\n }\r\n }\r\n}\r\n\r\n\r\n\n\n//# sourceURL=webpack:///./doc_viewer/api/canvas_factory.js?");
/***/ }),
/***/ "./doc_viewer/api/interfaces.js":
/*!**************************************!*\
!*** ./doc_viewer/api/interfaces.js ***!
\**************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ DocStruct: () => (/* binding */ DocStruct),\n/* harmony export */ IPageView: () => (/* binding */ IPageView),\n/* harmony export */ IPageViewFactory: () => (/* binding */ IPageViewFactory),\n/* harmony export */ IRender: () => (/* binding */ IRender),\n/* harmony export */ PageViewPort: () => (/* binding */ PageViewPort)\n/* harmony export */ });\n\r\nclass PageViewPort {\r\n #width;\r\n #height;\r\n #scale;\r\n #rotation;\r\n\r\n constructor({\r\n width = 0,\r\n height = 0,\r\n scale = 1,\r\n rotation = 0,\r\n }) {\r\n this.#width = width;\r\n this.#height = height;\r\n this.#scale = scale;\r\n this.#rotation = rotation;\r\n }\r\n\r\n get width() {\r\n return this.#width;\r\n }\r\n\r\n get height() {\r\n return this.#height;\r\n }\r\n\r\n get scale() {\r\n return this.#scale;\r\n }\r\n\r\n get rotation() {\r\n return this.#rotation;\r\n }\r\n}\r\n\r\nclass IRender {\r\n constructor() {\r\n }\r\n\r\n render() {\r\n const div = document.createElement('div');\r\n return div;\r\n }\r\n\r\n dispose() {\r\n }\r\n}\r\n\r\nclass IPageView extends IRender {\r\n constructor() {\r\n super();\r\n this.viewport = new PageViewPort({});\r\n }\r\n\r\n dispose() {\r\n super.dispose();\r\n }\r\n\r\n /**\r\n * @returns {Promise<void>}\r\n */\r\n draw() {\r\n }\r\n}\r\n\r\nclass IPageViewFactory {\r\n constructor() {\r\n }\r\n\r\n async createViews() {\r\n const pageView = new IPageView();\r\n return [pageView];\r\n }\r\n}\r\n\r\nclass DocStruct {\r\n #title;\r\n #factory;\r\n\r\n /**\r\n * @param {string} title\r\n * @param {IPageViewFactory | IPageViewFactory[]} factory\r\n */\r\n constructor(title, factory) {\r\n this.#title = title;\r\n this.#factory = factory;\r\n }\r\n\r\n get title() {\r\n return this.#title;\r\n }\r\n\r\n get factory() {\r\n return this.#factory;\r\n }\r\n\r\n async createPageViews() {\r\n if (Array.isArray(this.#factory)) {\r\n const buffer = [];\r\n for (const factory of this.#factory) {\r\n const result = await factory.createViews();\r\n buffer.push(...result);\r\n }\r\n\r\n return buffer;\r\n }\r\n\r\n return await this.#factory.createViews();\r\n }\r\n}\r\n\r\n\r\n\n\n//# sourceURL=webpack:///./doc_viewer/api/interfaces.js?");
/***/ }),
/***/ "./doc_viewer/api/views/images/image_page_view.js":
/*!********************************************************!*\
!*** ./doc_viewer/api/views/images/image_page_view.js ***!
\********************************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ ImagePageView: () => (/* binding */ ImagePageView)\n/* harmony export */ });\n/* harmony import */ var _interfaces_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../interfaces.js */ \"./doc_viewer/api/interfaces.js\");\n/* harmony import */ var _canvas_factory_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../canvas_factory.js */ \"./doc_viewer/api/canvas_factory.js\");\n\r\n\r\n\r\nclass ImagePageView extends _interfaces_js__WEBPACK_IMPORTED_MODULE_0__.IPageView {\r\n /** @type {HTMLImageElement | null} */\r\n #image;\r\n /** @type {string | null} */\r\n #src;\r\n\r\n /**\r\n * @param {CanvasImageSource} imageSource\r\n */\r\n constructor(imageSource) {\r\n super();\r\n this.imageSource = imageSource;\r\n this.#image = null;\r\n this.#src = null;\r\n this.#calcRectangle();\r\n }\r\n\r\n #calcRectangle() {\r\n if (\"naturalWidth\" in this.imageSource) {\r\n this.width = this.imageSource.naturalWidth;\r\n this.height = this.imageSource.naturalHeight;\r\n } else {\r\n this.width = this.imageSource.width;\r\n this.height = this.imageSource.height;\r\n }\r\n }\r\n\r\n render() {\r\n const div = super.render();\r\n div.style.width = `calc(${this.width}px)`;\r\n div.style.height = `calc(${this.height}px)`;\r\n this.#image ||= document.createElement('img');\r\n div.append(this.#image);\r\n return div;\r\n }\r\n\r\n async draw() {\r\n await super.draw();\r\n if (this.#src) return;\r\n\r\n const factory = new _canvas_factory_js__WEBPACK_IMPORTED_MODULE_1__.CanvasFactory();\r\n const { canvas, context } = factory.create(this.width, this.height);\r\n\r\n try {\r\n context.drawImage(this.imageSource, 0, 0);\r\n let blob = null;\r\n if (\"convertToBlob\" in canvas) {\r\n blob = await canvas.convertToBlob();\r\n } else {\r\n blob = await new Promise(resolve => canvas.toBlob(resolve));\r\n }\r\n\r\n this.#src = window.URL.createObjectURL(blob);\r\n const promise = Promise.withResolvers();\r\n\r\n this.#image.onload = promise.resolve;\r\n this.#image.onerror = promise.reject;\r\n this.#image.src = this.#src;\r\n\r\n await promise.promise;\r\n } finally {\r\n canvas.width = 0;\r\n canvas.height = 0;\r\n }\r\n }\r\n\r\n dispose() {\r\n super.dispose();\r\n if (this.#src) {\r\n window.URL.revokeObjectURL(this.#src);\r\n }\r\n }\r\n}\r\n\r\n\r\n\n\n//# sourceURL=webpack:///./doc_viewer/api/views/images/image_page_view.js?");
/***/ }),
/***/ "./doc_viewer/api/views/images/image_page_view_factory.js":
/*!****************************************************************!*\
!*** ./doc_viewer/api/views/images/image_page_view_factory.js ***!
\****************************************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ ImagePageViewFactory: () => (/* binding */ ImagePageViewFactory)\n/* harmony export */ });\n/* harmony import */ var _interfaces_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../interfaces.js */ \"./doc_viewer/api/interfaces.js\");\n/* harmony import */ var _image_page_view_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./image_page_view.js */ \"./doc_viewer/api/views/images/image_page_view.js\");\n\r\n\r\n\r\nclass ImagePageViewFactory extends _interfaces_js__WEBPACK_IMPORTED_MODULE_0__.IPageViewFactory {\r\n /**\r\n * @param {(string | Blob)[]} sources\r\n */\r\n constructor(sources) {\r\n super();\r\n this.sources = sources;\r\n }\r\n\r\n async createViews() {\r\n const views = [];\r\n for (const src of this.sources) {\r\n let blob = src;\r\n if (typeof src === \"string\") {\r\n blob = await fetch(src).then(response => response.blob());\r\n }\r\n\r\n const bitmap = await createImageBitmap(blob);\r\n views.push(new _image_page_view_js__WEBPACK_IMPORTED_MODULE_1__.ImagePageView(bitmap));\r\n }\r\n\r\n return views;\r\n }\r\n}\r\n\r\n\r\n\n\n//# sourceURL=webpack:///./doc_viewer/api/views/images/image_page_view_factory.js?");
/***/ }),
/***/ "./doc_viewer/web/app.js":
/*!*******************************!*\
!*** ./doc_viewer/web/app.js ***!
\*******************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ ViewerApplication: () => (/* binding */ ViewerApplication)\n/* harmony export */ });\n/* harmony import */ var _api_interfaces_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../api/interfaces.js */ \"./doc_viewer/api/interfaces.js\");\n/* harmony import */ var _components_document_factory_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./components/document_factory.js */ \"./doc_viewer/web/components/document_factory.js\");\n/* harmony import */ var _components_event_bus_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./components/event_bus.js */ \"./doc_viewer/web/components/event_bus.js\");\n/* harmony import */ var _components_file_selecter_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./components/file_selecter.js */ \"./doc_viewer/web/components/file_selecter.js\");\n/* harmony import */ var _components_rendering_queue_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./components/rendering_queue.js */ \"./doc_viewer/web/components/rendering_queue.js\");\n/* harmony import */ var _doc_viewer_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./doc_viewer.js */ \"./doc_viewer/web/doc_viewer.js\");\n\r\n\r\n\r\n\r\n\r\n\r\n\r\nclass ViewerApplication {\r\n #config;\r\n #eventBus;\r\n #viewer;\r\n\r\n constructor(config) {\r\n this.#config = config;\r\n this.#eventBus = new _components_event_bus_js__WEBPACK_IMPORTED_MODULE_2__.EventBus();\r\n this.#viewer = new _doc_viewer_js__WEBPACK_IMPORTED_MODULE_5__.DocViewer(this.#config.pageViewer, this.#eventBus);\r\n this.fileSelector = new _components_file_selecter_js__WEBPACK_IMPORTED_MODULE_3__.FileSelector(this.#config.fileSelector, this.#eventBus);\r\n this.renderingQueue = new _components_rendering_queue_js__WEBPACK_IMPORTED_MODULE_4__.RenderingQueue(this.#viewer, this.#eventBus);\r\n\r\n this.#initEvent();\r\n }\r\n\r\n /**\r\n * @param {DocStruct | null} doc\r\n */\r\n setDocument(doc) {\r\n this.#viewer.setDocument(doc);\r\n }\r\n\r\n #initEvent() {\r\n this.#eventBus.on('fileSelector:input', ({ files }) => {\r\n const factory = new _components_document_factory_js__WEBPACK_IMPORTED_MODULE_1__.DocumentFactory(files);\r\n this.setDocument(factory.create());\r\n });\r\n\r\n this.#eventBus.on('docViewer:ready', () => {\r\n console.log('ready viewer');\r\n });\r\n }\r\n}\r\n\r\n\r\n\n\n//# sourceURL=webpack:///./doc_viewer/web/app.js?");
/***/ }),
/***/ "./doc_viewer/web/components/document_factory.js":
/*!*******************************************************!*\
!*** ./doc_viewer/web/components/document_factory.js ***!
\*******************************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ DocumentFactory: () => (/* binding */ DocumentFactory)\n/* harmony export */ });\n/* harmony import */ var _api_interfaces_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../api/interfaces.js */ \"./doc_viewer/api/interfaces.js\");\n/* harmony import */ var _api_views_images_image_page_view_factory_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../api/views/images/image_page_view_factory.js */ \"./doc_viewer/api/views/images/image_page_view_factory.js\");\n\r\n\r\n\r\nclass DocumentFactory {\r\n /**\r\n * @param {Blob[]} files\r\n */\r\n constructor(files) {\r\n this.files = files;\r\n }\r\n\r\n create() {\r\n const factories = [];\r\n for (const file of this.files) {\r\n const ret = this.#ensure(file);\r\n factories.push(ret);\r\n }\r\n\r\n const docStruct = new _api_interfaces_js__WEBPACK_IMPORTED_MODULE_0__.DocStruct(\"\", factories);\r\n return docStruct;\r\n }\r\n\r\n /**\r\n * @param {Blob} file\r\n */\r\n #ensure(file) {\r\n switch (file.type) {\r\n case \"image/jpeg\":\r\n case \"image/png\":\r\n case \"image/gif\":\r\n case \"image/bmp\": {\r\n return new _api_views_images_image_page_view_factory_js__WEBPACK_IMPORTED_MODULE_1__.ImagePageViewFactory([file]);\r\n }\r\n default: {\r\n throw new Error(`Unknown file type.`);\r\n }\r\n }\r\n }\r\n}\r\n\r\n\r\n\n\n//# sourceURL=webpack:///./doc_viewer/web/components/document_factory.js?");
/***/ }),
/***/ "./doc_viewer/web/components/event_bus.js":
/*!************************************************!*\
!*** ./doc_viewer/web/components/event_bus.js ***!
\************************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ EventBus: () => (/* binding */ EventBus)\n/* harmony export */ });\n\r\nclass EventBus {\r\n /** @type {Map<string, Function[]>} */\r\n #listeners;\r\n\r\n constructor() {\r\n this.#listeners = new Map();\r\n }\r\n\r\n on(eventName, eventListener) {\r\n const stack = this.#ensureEventStack(eventName);\r\n stack.push(eventListener);\r\n }\r\n\r\n off(eventName, eventListener) {\r\n const stack = this.#ensureEventStack(eventName);\r\n const ix = stack.indexOf(eventListener);\r\n if (ix !== -1) {\r\n stack.splice(ix, 1);\r\n }\r\n }\r\n\r\n dispatch(eventName, eventDetails) {\r\n for (const handler of this.#ensureEventStack(eventName).slice()) {\r\n handler?.(eventDetails);\r\n }\r\n }\r\n\r\n /**\r\n * @param {string} eventName\r\n */\r\n #ensureEventStack(eventName) {\r\n if (!this.#listeners.has(eventName)) {\r\n this.#listeners.set(eventName, []);\r\n }\r\n\r\n return this.#listeners.get(eventName);\r\n }\r\n}\r\n\r\n\r\n\n\n//# sourceURL=webpack:///./doc_viewer/web/components/event_bus.js?");
/***/ }),
/***/ "./doc_viewer/web/components/file_selecter.js":
/*!****************************************************!*\
!*** ./doc_viewer/web/components/file_selecter.js ***!
\****************************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ FileSelector: () => (/* binding */ FileSelector)\n/* harmony export */ });\n/* harmony import */ var _event_bus_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./event_bus.js */ \"./doc_viewer/web/components/event_bus.js\");\n\r\n\r\nclass FileSelector {\r\n #fileInput;\r\n #openButton;\r\n #eventBus;\r\n\r\n /**\r\n * @param {Object} options\r\n * @param {HTMLInputElement} options.fileInput\r\n * @param {HTMLButtonElement} options.openButton\r\n * @param {EventBus} eventBus\r\n */\r\n constructor({\r\n fileInput,\r\n openButton\r\n }, eventBus) {\r\n this.#fileInput = fileInput;\r\n this.#openButton = openButton;\r\n this.#eventBus = eventBus;\r\n this.#initialize();\r\n }\r\n\r\n #initialize() {\r\n this.#openButton.addEventListener('click', e => {\r\n this.#fileInput.click();\r\n });\r\n\r\n this.#fileInput.addEventListener('input', e => {\r\n const files = this.#fileInput.files;\r\n if (!files?.[0]) {\r\n return;\r\n }\r\n\r\n this.#eventBus.dispatch('fileSelector:input', {\r\n files: [...files],\r\n });\r\n this.#fileInput.value = \"\";\r\n });\r\n }\r\n}\r\n\r\n\r\n\n\n//# sourceURL=webpack:///./doc_viewer/web/components/file_selecter.js?");
/***/ }),
/***/ "./doc_viewer/web/components/rendering_queue.js":
/*!******************************************************!*\
!*** ./doc_viewer/web/components/rendering_queue.js ***!
\******************************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ RenderingQueue: () => (/* binding */ RenderingQueue)\n/* harmony export */ });\n/* harmony import */ var _doc_viewer_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../doc_viewer.js */ \"./doc_viewer/web/doc_viewer.js\");\n/* harmony import */ var _event_bus_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./event_bus.js */ \"./doc_viewer/web/components/event_bus.js\");\n\r\n\r\n\r\nclass RenderingQueue {\r\n #viewer;\r\n #eventBus;\r\n\r\n /**\r\n * @param {DocViewer} viewer\r\n * @param {EventBus} eventBus\r\n */\r\n constructor(viewer, eventBus) {\r\n this.#viewer = viewer;\r\n this.#eventBus = eventBus;\r\n\r\n this.#bindEvents();\r\n }\r\n\r\n #bindEvents() {\r\n this.#eventBus.on('docViewer:ready', () => {\r\n this.#ensureTask();\r\n });\r\n }\r\n\r\n async #ensureTask(p = 0) {\r\n const view = this.#viewer.getView(p);\r\n if (!view) return;\r\n await view.draw();\r\n return this.#ensureTask(p++);\r\n }\r\n}\r\n\r\n\r\n\n\n//# sourceURL=webpack:///./doc_viewer/web/components/rendering_queue.js?");
/***/ }),
/***/ "./doc_viewer/web/doc_viewer.js":
/*!**************************************!*\
!*** ./doc_viewer/web/doc_viewer.js ***!
\**************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ DocViewer: () => (/* binding */ DocViewer)\n/* harmony export */ });\n/* harmony import */ var _api_interfaces_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../api/interfaces.js */ \"./doc_viewer/api/interfaces.js\");\n/* harmony import */ var _components_event_bus_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./components/event_bus.js */ \"./doc_viewer/web/components/event_bus.js\");\n\r\n\r\n\r\nclass DocViewer {\r\n /** @type {DocStruct | null} */\r\n #document;\r\n /** @type {IPageView[]} */\r\n #views;\r\n #eventBus;\r\n #options;\r\n\r\n /**\r\n * @param {Record<string, HTMLElement>} options\r\n * @param {EventBus} eventBus\r\n */\r\n constructor(options, eventBus) {\r\n this.#document = null;\r\n this.#views = [];\r\n this.#eventBus = eventBus;\r\n this.#options = options;\r\n }\r\n\r\n /**\r\n * @param {DocStruct | null} document\r\n */\r\n setDocument(document) {\r\n if (this.#document) {\r\n for (const view of this.#views) {\r\n view.dispose();\r\n }\r\n this.#views.length = 0;\r\n }\r\n\r\n this.#document = document;\r\n if (!document) {\r\n return;\r\n }\r\n\r\n document.createPageViews().then(pageViews => {\r\n this.#views.length = 0;\r\n for (const view of pageViews) {\r\n this.#views.push(view);\r\n }\r\n return this.#initialize();\r\n }).then(() => {\r\n this.#eventBus.dispatch('docViewer:ready');\r\n });\r\n }\r\n\r\n async #initialize() {\r\n for (const view of this.#views) {\r\n const div = view.render();\r\n this.#options.container.append(div);\r\n }\r\n }\r\n\r\n get pagesCount() {\r\n return this.#views.length;\r\n }\r\n\r\n getView(pageNumber) {\r\n return this.#views.at(pageNumber);\r\n }\r\n\r\n async drawAll() {\r\n for (const view of this.#views) {\r\n await view.draw();\r\n }\r\n }\r\n}\r\n\r\n\r\n\n\n//# sourceURL=webpack:///./doc_viewer/web/doc_viewer.js?");
/***/ }),
/***/ "./doc_viewer/web/viewer.js":
/*!**********************************!*\
!*** ./doc_viewer/web/viewer.js ***!
\**********************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _app_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./app.js */ \"./doc_viewer/web/app.js\");\n\r\n\r\nfunction getViewerConfiguration() {\r\n return {\r\n pageViewer: {\r\n container: document.getElementById('page-viewer'),\r\n },\r\n fileSelector: {\r\n container: document.getElementById('file-selector'),\r\n openButton: document.getElementById('file-selector-button'),\r\n fileInput: document.getElementById('file-selector-input'),\r\n },\r\n };\r\n}\r\n\r\nfunction onLoaded() {\r\n const viewer = getViewerConfiguration();\r\n const app = new _app_js__WEBPACK_IMPORTED_MODULE_0__.ViewerApplication(viewer);\r\n window.app = app;\r\n return app;\r\n}\r\n\r\nif (document.readyState === \"loading\") {\r\n document.addEventListener('DOMContentLoaded', onLoaded);\r\n} else {\r\n onLoaded();\r\n}\r\n\n\n//# sourceURL=webpack:///./doc_viewer/web/viewer.js?");
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/************************************************************************/
/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module can't be inlined because the eval devtool is used.
/******/ var __webpack_exports__ = __webpack_require__("./doc_viewer/web/viewer.js");
/******/
/******/ })()
;

0
src/dist/viewer.css vendored Normal file
View File

25
src/dist/viewer.html vendored Normal file
View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Doc Viewer</title>
<link rel="stylesheet" href="./viewer.css" />
<script src="./bundle.js" defer></script>
</head>
<body>
<div class="viewer-container">
<div class="toolbar-container">
<div class="file-selector-component" id="file-selector">
<button type="button" id="file-selector-button"></button>
<input type="file" id="file-selector-input" />
</div>
</div>
<div class="page-container">
<div id="page-viewer"></div>
</div>
<div class="bottom-toolbar-container">
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,32 @@
class CanvasFactory {
/**
* @param {number} width
* @param {number} height
*/
create(width, height) {
if (typeof OffscreenCanvas !== "undefined") {
const canvas = new OffscreenCanvas(width, height);
const context = canvas.getContext('2d');
return {
canvas,
context,
};
} else if (typeof document !== "undefined") {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const context = canvas.getContext('2d');
return {
canvas,
context,
};
} else {
throw new Error(`Not implemented`);
}
}
}
export {
CanvasFactory,
}

View File

@ -0,0 +1,119 @@
class PageViewPort {
#width;
#height;
#scale;
#rotation;
constructor({
width = 0,
height = 0,
scale = 1,
rotation = 0,
}) {
this.#width = width;
this.#height = height;
this.#scale = scale;
this.#rotation = rotation;
}
get width() {
return this.#width;
}
get height() {
return this.#height;
}
get scale() {
return this.#scale;
}
get rotation() {
return this.#rotation;
}
}
class IRender {
constructor() {
}
render() {
const div = document.createElement('div');
return div;
}
dispose() {
}
}
class IPageView extends IRender {
constructor() {
super();
this.viewport = new PageViewPort({});
}
dispose() {
super.dispose();
}
/**
* @returns {Promise<void>}
*/
draw() {
}
}
class IPageViewFactory {
constructor() {
}
async createViews() {
const pageView = new IPageView();
return [pageView];
}
}
class DocStruct {
#title;
#factory;
/**
* @param {string} title
* @param {IPageViewFactory | IPageViewFactory[]} factory
*/
constructor(title, factory) {
this.#title = title;
this.#factory = factory;
}
get title() {
return this.#title;
}
get factory() {
return this.#factory;
}
async createPageViews() {
if (Array.isArray(this.#factory)) {
const buffer = [];
for (const factory of this.#factory) {
const result = await factory.createViews();
buffer.push(...result);
}
return buffer;
}
return await this.#factory.createViews();
}
}
export {
PageViewPort,
IRender,
IPageView,
IPageViewFactory,
DocStruct,
}

View File

@ -0,0 +1,80 @@
import { IPageView } from "../../interfaces.js";
import { CanvasFactory } from "../../canvas_factory.js";
class ImagePageView extends IPageView {
/** @type {HTMLImageElement | null} */
#image;
/** @type {string | null} */
#src;
/**
* @param {CanvasImageSource} imageSource
*/
constructor(imageSource) {
super();
this.imageSource = imageSource;
this.#image = null;
this.#src = null;
this.#calcRectangle();
}
#calcRectangle() {
if ("naturalWidth" in this.imageSource) {
this.width = this.imageSource.naturalWidth;
this.height = this.imageSource.naturalHeight;
} else {
this.width = this.imageSource.width;
this.height = this.imageSource.height;
}
}
render() {
const div = super.render();
div.style.width = `calc(${this.width}px)`;
div.style.height = `calc(${this.height}px)`;
this.#image ||= document.createElement('img');
div.append(this.#image);
return div;
}
async draw() {
await super.draw();
if (this.#src) return;
const factory = new CanvasFactory();
const { canvas, context } = factory.create(this.width, this.height);
try {
context.drawImage(this.imageSource, 0, 0);
let blob = null;
if ("convertToBlob" in canvas) {
blob = await canvas.convertToBlob();
} else {
blob = await new Promise(resolve => canvas.toBlob(resolve));
}
this.#src = window.URL.createObjectURL(blob);
const promise = Promise.withResolvers();
this.#image.onload = promise.resolve;
this.#image.onerror = promise.reject;
this.#image.src = this.#src;
await promise.promise;
} finally {
canvas.width = 0;
canvas.height = 0;
}
}
dispose() {
super.dispose();
if (this.#src) {
window.URL.revokeObjectURL(this.#src);
}
}
}
export {
ImagePageView,
}

View File

@ -0,0 +1,31 @@
import { IPageViewFactory } from "../../interfaces.js";
import { ImagePageView } from "./image_page_view.js";
class ImagePageViewFactory extends IPageViewFactory {
/**
* @param {(string | Blob)[]} sources
*/
constructor(sources) {
super();
this.sources = sources;
}
async createViews() {
const views = [];
for (const src of this.sources) {
let blob = src;
if (typeof src === "string") {
blob = await fetch(src).then(response => response.blob());
}
const bitmap = await createImageBitmap(blob);
views.push(new ImagePageView(bitmap));
}
return views;
}
}
export {
ImagePageViewFactory,
}

44
src/doc_viewer/web/app.js Normal file
View File

@ -0,0 +1,44 @@
import { DocStruct } from "../api/interfaces.js";
import { DocumentFactory } from "./components/document_factory.js";
import { EventBus } from "./components/event_bus.js";
import { FileSelector } from "./components/file_selecter.js";
import { RenderingQueue } from "./components/rendering_queue.js";
import { DocViewer } from "./doc_viewer.js";
class ViewerApplication {
#config;
#eventBus;
#viewer;
constructor(config) {
this.#config = config;
this.#eventBus = new EventBus();
this.#viewer = new DocViewer(this.#config.pageViewer, this.#eventBus);
this.fileSelector = new FileSelector(this.#config.fileSelector, this.#eventBus);
this.renderingQueue = new RenderingQueue(this.#viewer, this.#eventBus);
this.#initEvent();
}
/**
* @param {DocStruct | null} doc
*/
setDocument(doc) {
this.#viewer.setDocument(doc);
}
#initEvent() {
this.#eventBus.on('fileSelector:input', ({ files }) => {
const factory = new DocumentFactory(files);
this.setDocument(factory.create());
});
this.#eventBus.on('docViewer:ready', () => {
console.log('ready viewer');
});
}
}
export {
ViewerApplication,
}

View File

@ -0,0 +1,43 @@
import { DocStruct } from "../../api/interfaces.js";
import { ImagePageViewFactory } from "../../api/views/images/image_page_view_factory.js";
class DocumentFactory {
/**
* @param {Blob[]} files
*/
constructor(files) {
this.files = files;
}
create() {
const factories = [];
for (const file of this.files) {
const ret = this.#ensure(file);
factories.push(ret);
}
const docStruct = new DocStruct("", factories);
return docStruct;
}
/**
* @param {Blob} file
*/
#ensure(file) {
switch (file.type) {
case "image/jpeg":
case "image/png":
case "image/gif":
case "image/bmp": {
return new ImagePageViewFactory([file]);
}
default: {
throw new Error(`Unknown file type.`);
}
}
}
}
export {
DocumentFactory,
}

View File

@ -0,0 +1,43 @@
class EventBus {
/** @type {Map<string, Function[]>} */
#listeners;
constructor() {
this.#listeners = new Map();
}
on(eventName, eventListener) {
const stack = this.#ensureEventStack(eventName);
stack.push(eventListener);
}
off(eventName, eventListener) {
const stack = this.#ensureEventStack(eventName);
const ix = stack.indexOf(eventListener);
if (ix !== -1) {
stack.splice(ix, 1);
}
}
dispatch(eventName, eventDetails) {
for (const handler of this.#ensureEventStack(eventName).slice()) {
handler?.(eventDetails);
}
}
/**
* @param {string} eventName
*/
#ensureEventStack(eventName) {
if (!this.#listeners.has(eventName)) {
this.#listeners.set(eventName, []);
}
return this.#listeners.get(eventName);
}
}
export {
EventBus,
}

View File

@ -0,0 +1,45 @@
import { EventBus } from "./event_bus.js";
class FileSelector {
#fileInput;
#openButton;
#eventBus;
/**
* @param {Object} options
* @param {HTMLInputElement} options.fileInput
* @param {HTMLButtonElement} options.openButton
* @param {EventBus} eventBus
*/
constructor({
fileInput,
openButton
}, eventBus) {
this.#fileInput = fileInput;
this.#openButton = openButton;
this.#eventBus = eventBus;
this.#initialize();
}
#initialize() {
this.#openButton.addEventListener('click', e => {
this.#fileInput.click();
});
this.#fileInput.addEventListener('input', e => {
const files = this.#fileInput.files;
if (!files?.[0]) {
return;
}
this.#eventBus.dispatch('fileSelector:input', {
files: [...files],
});
this.#fileInput.value = "";
});
}
}
export {
FileSelector,
}

View File

@ -0,0 +1,35 @@
import { DocViewer } from "../doc_viewer.js";
import { EventBus } from "./event_bus.js";
class RenderingQueue {
#viewer;
#eventBus;
/**
* @param {DocViewer} viewer
* @param {EventBus} eventBus
*/
constructor(viewer, eventBus) {
this.#viewer = viewer;
this.#eventBus = eventBus;
this.#bindEvents();
}
#bindEvents() {
this.#eventBus.on('docViewer:ready', () => {
this.#ensureTask();
});
}
async #ensureTask(p = 0) {
const view = this.#viewer.getView(p);
if (!view) return;
await view.draw();
return this.#ensureTask(p++);
}
}
export {
RenderingQueue,
}

View File

@ -0,0 +1,74 @@
import { DocStruct, IPageView } from "../api/interfaces.js";
import { EventBus } from "./components/event_bus.js";
class DocViewer {
/** @type {DocStruct | null} */
#document;
/** @type {IPageView[]} */
#views;
#eventBus;
#options;
/**
* @param {Record<string, HTMLElement>} options
* @param {EventBus} eventBus
*/
constructor(options, eventBus) {
this.#document = null;
this.#views = [];
this.#eventBus = eventBus;
this.#options = options;
}
/**
* @param {DocStruct | null} document
*/
setDocument(document) {
if (this.#document) {
for (const view of this.#views) {
view.dispose();
}
this.#views.length = 0;
}
this.#document = document;
if (!document) {
return;
}
document.createPageViews().then(pageViews => {
this.#views.length = 0;
for (const view of pageViews) {
this.#views.push(view);
}
return this.#initialize();
}).then(() => {
this.#eventBus.dispatch('docViewer:ready');
});
}
async #initialize() {
for (const view of this.#views) {
const div = view.render();
this.#options.container.append(div);
}
}
get pagesCount() {
return this.#views.length;
}
getView(pageNumber) {
return this.#views[pageNumber];
}
async drawAll() {
for (const view of this.#views) {
await view.draw();
}
}
}
export {
DocViewer,
}

View File

View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Doc Viewer</title>
<link rel="stylesheet" href="./viewer.css" />
<script src="./viewer.js" type="module" defer></script>
</head>
<body>
<div class="viewer-container">
<div class="toolbar-container">
<div class="file-selector-component" id="file-selector">
<button type="button" id="file-selector-button"></button>
<input type="file" id="file-selector-input" />
</div>
</div>
<div class="page-container">
<div id="page-viewer"></div>
</div>
<div class="bottom-toolbar-container">
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,27 @@
import { ViewerApplication } from "./app.js";
function getViewerConfiguration() {
return {
pageViewer: {
container: document.getElementById('page-viewer'),
},
fileSelector: {
container: document.getElementById('file-selector'),
openButton: document.getElementById('file-selector-button'),
fileInput: document.getElementById('file-selector-input'),
},
};
}
function onLoaded() {
const viewer = getViewerConfiguration();
const app = new ViewerApplication(viewer);
window.app = app;
return app;
}
if (document.readyState === "loading") {
document.addEventListener('DOMContentLoaded', onLoaded);
} else {
onLoaded();
}

3806
src/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

13
src/package.json Normal file
View File

@ -0,0 +1,13 @@
{
"scripts": {
"dev": "webpack serve --config webpack.config.js --hot",
"build": "webpack --config webpack.config.js"
},
"dependencies": {
"webpack": "^5.96.1"
},
"devDependencies": {
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.1.0"
}
}

29
src/webpack.config.js Normal file
View File

@ -0,0 +1,29 @@
const path = require("path");
/** @type {import("webpack").Configuration} */
module.exports = {
entry: './doc_viewer/web/viewer.js',
output: {
filename: 'bundle.js', // contenthashは使用しない
path: path.resolve(__dirname, './dist'),
publicPath: ''
},
mode: 'development', // development
devServer: {
static: {
directory: path.join(__dirname, '/doc_viewer'),
},
compress: true,
port: 9000,
},
module: {
// rules: [
// {
// test: /\.css$/,
// use: [
// 'style-loader', 'css-loader' // MiniCssExtractPluginを使用しない
// ]
// },
// ]
},
};