{"id":18248828,"url":"https://github.com/igorski/bitmappery","last_synced_at":"2025-04-05T01:03:20.491Z","repository":{"id":39547475,"uuid":"320360913","full_name":"igorski/bitmappery","owner":"igorski","description":"Browser based non-destructive image editor with layering, masking, customizable brushes and cloud storage integration for all your online editing.","archived":false,"fork":false,"pushed_at":"2025-03-28T21:00:37.000Z","size":2993,"stargazers_count":168,"open_issues_count":9,"forks_count":22,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-03-28T21:32:35.908Z","etag":null,"topics":["browser","image-editor","image-processing","photo-editor","photos","photoshop","pwa","pwa-apps","typescript","vue","vuejs","wasm","webassembly"],"latest_commit_sha":null,"homepage":"https://www.igorski.nl/bitmappery","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/igorski.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-12-10T18:47:05.000Z","updated_at":"2025-03-28T20:57:14.000Z","dependencies_parsed_at":"2024-01-01T12:32:02.326Z","dependency_job_id":"dbbeb0a1-2dcf-4784-beb1-3a69d8d04b86","html_url":"https://github.com/igorski/bitmappery","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igorski%2Fbitmappery","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igorski%2Fbitmappery/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igorski%2Fbitmappery/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igorski%2Fbitmappery/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/igorski","download_url":"https://codeload.github.com/igorski/bitmappery/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247271514,"owners_count":20911587,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["browser","image-editor","image-processing","photo-editor","photos","photoshop","pwa","pwa-apps","typescript","vue","vuejs","wasm","webassembly"],"created_at":"2024-11-05T09:38:25.877Z","updated_at":"2025-04-05T01:03:20.477Z","avatar_url":"https://github.com/igorski.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# BitMappery\n\n## So you are rebuilding Photoshop in the browser ?\n\nNo, I'm building a tool that does the bare minimum of what I require and what I don't\nfind in other open source tools (or rather: _not entirely to my preference_). That doesn't mean of course\nthat contributions related to Photoshop-esque features aren't welcomed ;-)\n\n### All hand-written ?\n\nYep, but as the author has worked in the photo software industry, BitMappery had a head start as certain challenges had\nbeen tackled before. Also, BitMappery is reusing igorski's [zCanvas](https://github.com/igorski/zCanvas) under the hood for rendering\nand bitmap blitting. The application is written on top of [Vue](https://github.com/vuejs/vue) using [Vuex](https://github.com/vuejs/vuex) for state management.\n\n## The [Issue Tracker](https://github.com/igorski/bitmappery/issues) is your point of contact\n\nBug reports, feature requests, questions and discussions are welcome on the GitHub Issue Tracker, please do not send e-mails through the development website. However, please search before posting to avoid duplicates, and limit to one issue per post.\n\nPlease vote on feature requests by using the Thumbs Up/Down reaction on the first post.\n\n## Model\n\nBitMappery works with entities known as `Document`s. A Document contains several `Layer`s, each of\nwhich define their content, transformation, `Effect`s, etc. Each of the nested entity properties\nhas its own factory (see `src/factories`). The Document is managed by the Vuex `document-module.ts`.\n\nThe types for each of these are defined in `src/definitions/document.ts`.\n\n## Document rendering and interactions\n\nThe Document is rendered one layer at a time onto a Canvas element, using [zCanvas](https://github.com/igorski/zCanvas). Both the rendering and interaction handling is performed by dedicated \"Sprite\" classes, which function as _renderers_ for the Documents _actors_.\nIn other words: _renderers represent the Document visually and handle interactions modifying the Document state_.\n\nAll layer rendering and layer interactions are handled by `src/rendering/actors/layer-renderer.ts`.\nNote that the purpose of the renderer is solely to delegate interactions events to the Layer entity. The\nrenderer should represent the properties of the Layer, the Layer should never reverse-engineer from the onscreen\ncontent (especially as different window size and scaling factor will greatly complicate these matters when\nperformed two-way).\n\nAll interactions that work across layers (viewport panning, layer selection by clicking on non-transparent\npixels and drawing of selections) is handled by a single top level Sprite that covers the entire zCanvas area.\nThis Sprite is `src/rendering/actors/interaction-pane.ts`.\n\nInteractions that start/end from _outside the canvas_ (for instance the opening/closing of a selection or the\ndrawing of a brush stroke outside of the canvas area) are handled by `document-canvas.vue` where the global DOM coordinates are translated to coordinates relative to the canvas document before being forwarded to the zCanvas\nevent handler. See \"Rendering concepts\" below for more details on screen-to-document coordinates.\n\nRendering of transformations, text and effects is an asynchronous operation handled by `src/services/render-service.ts`. The purpose of this service is to perform and cache repeated operations and eventually maintain\nthe source bitmap represented by the LayerRenderer. The LayerRenderer invokes the rendering service whenever\nLayer content changes and manages its own cache.\n\nAll types related to the editor are either defined in `src/definitions/editor.ts` or the more specifically\nnamed files.\n\n### Rendering concepts\n\nBitMappery follows the concepts of the display list as listed in the zCanvas wiki, where BitMappery's\ndocument layers are visualized as separate Sprites which can be manipulated as separate interactive on-screen\nelements. BitMappery additionally adds additional logic related to the viewing of large scale content in smaller fragments.\n\nThe zCanvas' DOM element (an _HTMLCanvasElement_ instance) is basically as large as the available area inside the\nDOM window allows. The BitMappery document displayed inside may however be larger or smaller than the canvas itself\n(depending on the _zoom level_ which is - not yet - standardized in the zCanvas package and custom written for BitMappery using the `ZoomableCanvas` and `ZoomableSprite` classes).\n\nWhat determines the visible area of the zoomed document is the _viewport_. As such, interactions with the zCanvas\nelement must be _translated_ from global DOM coordinates to a point relative to the BitMappery document, taking\ninto account the current scaling factor and viewport offset. This is handled automatically by all event handlers\ndelegated through zCanvas and the renderers, but needs care when performing rendering operations (such as drawing)\nand translating these to (non-zoomed and non-panned) source bitmaps.\n\n## State history\n\nMutations can be registered in state history (Vuex `history-module.ts`) in order to provide undo and redo\nof operations. In order to prevent storing a lot of changes of the same property (for instance when dragging a slider), the storage of a new state is deferred through a queue. This is why history states are enqueued by _propertyName_:\n\nWhen enqueuing a new state while there is an existing one enqueued for the same property name, the first state is updated so its redo will match that of the newest state, the undo remaining unchanged. The second state will not\nbe added to the queue.\n\nIt is good to understand that the undo/redo for an action should be considered separate\nfrom the Vue component that is triggering the transaction, the reason being that the component can be\nunmounted at the moment the history state is changed (and the component is no longer active).\n\nThat's why undo/redo handlers should either work on variables in a local scope, or on the Vuex store\nwhen mutating store properties. When relying on store state and getters, be sure to cache their\nvalues in the local scope to avoid conflicts (for instance in below example we cache _activeLayerIndex_\nas it is used by the undo/redo methods to update a specific Layer. _activeLayerIndex_ can change during\nthe application lifetime before the undo/redo handler fires which would otherwise lead to the _wrong Layer_\nbeing updated.\n\n```typescript\nupdate( propertyName: string, newValue: any ): void {\n    // cache the existing values of the property value we are about to mutate...\n    const existingValue = this.getterForExistingValue;\n    // ...and the layer index that is used to identify the layer containing the property\n    const index = this.activeLayerIndex;\n    const store: Store\u003cBitMapperyState\u003e = this.$store;\n    // define the method that will mutate the existing value to given newValue\n    const commit = (): void =\u003e store.commit( \"updateLayer\", { index, opts: { newValue } });\n    // and perform the mutation directly\n    commit();\n    // now define and enqueue undo/redo handlers to reverse and redo the commit mutation\n    enqueueState( propertyName, {\n        undo(): void {\n            store.commit( \"updateLayerTransform\", { index, opts: { existingValue } });\n        },\n        redo(): void {\n            commit();\n        },\n    });\n}\n```\n\nWhenever an action (that requires an undo state) can be triggered in multiple locations (for instance\ninside a component and as a keyboard shortcut in `src/services/keyboard-service`), you can\ncreate a custom handler inside `src/store/actions` to avoid code duplication.\n\n## Third party storage integration\n\nRequires you to register a client id or access token in the developer portal of the third party\nstorage provider. Currently, there is support for [Dropbox](https://www.dropbox.com/developers/apps), [Google Drive](https://console.cloud.google.com/) and S3 storage.\n\nYou can enable each of these integrations by providing the required key values for your configuration\nby creating a local `.env.local`-file which will contain your custom configuration. You can create this\nfile by duplicating the contents of the `.env`-file provided in the repository.\n\n## Project setup\n\nThe project setup is two-fold. You can get all the dependencies through NPM as usual:\n\n```\nnpm install\n```\n\nafter which you can run:\n\n* ```npm run dev``` to start a local development server with hot module reload\n* ```npm run build``` to compile a production package\n* ```npm run test```  to run the unit tests\n* ```npm run lint``` to run the linter on the source files\n\nThe above will suffice when working solely on the JavaScript side of things.\n\n## Docker based self hosted version\n\n#### Step 1 : Clone the BitMappery project into a local folder :\n\n```bash\ngit clone https://github.com/igorski/bitmappery.git\n```\n\n#### Step 2 : Build the image using the provided Dockerfile :\n\n```bash\ndocker build -t bitmappery .\n```\n\n#### Step 3 : Once the image is built, run the container and bind the ports :\n\n```bash\ndocker run -d -p 5173:5173 --name bitmappery-container bitmappery\n```\n\nOnce the container is started, you can access BitMappery at `http://localhost:5173`\n\n## WebAssembly\n\nBitMappery can also use WebAssembly to _potentially_ increase performance of image manipulation.\nWebAssembly filtering is a user controllable feature in the preferences pane, as long as the `.env` file has set\nsupport for `VITE_ENABLE_WASM_FILTERS` to true.\n\nThe source code is C based and compiled to WASM using [Emscripten](https://github.com/emscripten-core/emscripten). Because this setup is a little more cumbersome, the repository contains precompiled binaries in the `src/wasm/bin`-folder meaning you can\nomit this setup if you don't intend to make changes to these sources.\n\nIf you do wish to make contributions on this end, to compile the source (`src/wasm`) C-code to WASM, you\nwill first need to prepare your environment (note the last _source_ call does not permanently update your paths):\n\n```bash\ngit clone https://github.com/emscripten-core/emsdk.git\ncd emsdk\n./emsdk install latest\n./emsdk activate latest\nsource ./emsdk_env.sh\n```\n\nnow you can compile all source files to WASM using:\n\n```\nnpm run wasm\n```\n\n### Benchmarks\n\nOn a particular (deliberately low powered) configuration, running all filters at their heaviest setting on a particular source takes:\n\n* 7000+ ms in JavaScript\n* 558 ms in WebAssembly\n* 484 ms in JavaScript inside a Web Worker\n* 603 ms in WebAssembly inside a Web Worker\n\nNote that the WebAssembly Web Worker takes a performance hit from converting the ImageData buffer\nto float32 prior to allocating the buffer in the WASM instance's memory. This could benefit from\nfurther tweaking to see if it gets closer to the JavaScript Web Worker performance.\n\nHowever, as in the current setup the JS solution alone is performant enough _and you would need to write the\nfilter code twice_ (once in TypeScript in `src/rendering/filters` and once in C++ in `src/wasm`), the default for WASM is disabled.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Figorski%2Fbitmappery","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Figorski%2Fbitmappery","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Figorski%2Fbitmappery/lists"}