Compare commits

...

62 Commits

Author SHA1 Message Date
Tomáš Mládek c89080ff00
autoformat 2022-01-19 22:00:29 +01:00
Tomáš Mládek f24047b33b
fix canvas version mismatch breaking build 2022-01-19 20:52:02 +01:00
Tomáš Mládek f004daaa3c add docker deno build to Makefile, clean step, fix app build, .gitignore 2021-09-23 12:38:41 +02:00
Tomáš Mládek ff39c8eeb3 bot error reporting does not use colors & truncates at 2500 chars 2021-09-22 23:57:42 +02:00
Tomáš Mládek cf19301f96 add 60s timeout to bot commands to prevent lockups 2021-09-22 23:57:06 +02:00
Tomáš Mládek a5068e4b64 fix bot --rr flag; tidy up bot /help & /start; cosmetics 2021-09-22 23:56:42 +02:00
Tomáš Mládek 04ba5b7e4e show used images in CLI and bot; caption cmdline in bot 2021-09-22 23:55:48 +02:00
Tomáš Mládek 546102205e fix recursive defaults 2021-09-20 23:06:11 +02:00
Tomáš Mládek fd06a2a618 add recursive args to bot as well 2021-09-20 23:05:24 +02:00
Tomáš Mládek e391b9b6bf add cli params for recursive modes 2021-09-20 22:56:02 +02:00
Tomáš Mládek bfe739a487 recursive traversal is implicit 2021-09-20 22:43:58 +02:00
Tomáš Mládek 85881f877c cli - reformat & lint 2021-09-20 22:41:00 +02:00
Tomáš Mládek 4d8b0b85dd cli - use proper defaults 2021-09-20 22:40:12 +02:00
Tomáš Mládek 33603eabff cli - allow multiple modes to be selected, recursive collages
also use module augmentation to simplify types and all

TODO: make web app also use the same recursive code
2021-09-20 22:38:04 +02:00
Tomáš Mládek 2647238410 check both cases for directories 2021-09-19 13:47:52 +02:00
Tomáš Mládek 405fd3cad5 checks for nonexistent dirs 2021-09-19 13:44:35 +02:00
Tomáš Mládek 9a13c3c393 ...and -n 2021-09-19 13:36:31 +02:00
Tomáš Mládek 1e1faaa4c9 fix width/height args 2021-09-19 13:34:29 +02:00
Tomáš Mládek 0edc9a7ab4 fix args 2021-09-19 13:32:50 +02:00
Tomáš Mládek 8915a36b3d actual cmdline parsing in tg 2021-09-19 13:19:00 +02:00
Tomáš Mládek d443fc6d7e fix persistence (again) 2021-09-19 12:21:20 +02:00
Tomáš Mládek 6091c19c8b update gitignore 2021-09-19 12:19:31 +02:00
Tomáš Mládek b74d5838eb fix persistence 2021-09-19 12:18:32 +02:00
Tomáš Mládek 918e021eed fix lint 2021-09-19 12:14:41 +02:00
Tomáš Mládek 8ea2782038 remove clamping down files, improve logging 2021-09-19 12:09:17 +02:00
Tomáš Mládek 7e41fe054e autoformat & reasonable /start command 2021-09-19 12:09:15 +02:00
Tomáš Mládek 7e6a761363 lazy load images in deno 2021-09-19 12:08:46 +02:00
Tomáš Mládek 7919e4fb7d makefile improvements 2021-09-19 11:30:28 +02:00
Tomáš Mládek e82fcd5d42 error logging 2021-09-19 11:30:17 +02:00
Tomáš Mládek c89777621a unstable in .gitlab-ci.yml 2021-09-19 10:34:09 +02:00
Tomáš Mládek 3409978203 rudimentary tg bot 2021-09-19 10:31:34 +02:00
Tomáš Mládek d8dff75411 deno build --unstable 2021-09-19 09:48:25 +02:00
Tomáš Mládek 944dc9ca5f add a --recursive option to deno cli 2021-09-18 19:04:29 +02:00
Tomáš Mládek 26ceccbde2 formatting 2021-09-18 18:58:16 +02:00
Tomáš Mládek e34c3a96c4 pin specific canvas version in deno 2021-09-18 18:57:38 +02:00
Tomáš Mládek 86c9644972 -m param without args list modes 2021-09-18 12:11:03 +02:00
Tomáš Mládek e9bc9f568d limit cli only to jpgs/pngs (by default) 2021-09-18 11:42:44 +02:00
Tomáš Mládek eb455e2311 clamp cli to 25 images for performance reasons 2021-09-17 19:09:51 +02:00
Tomáš Mládek 57f32ef643 cli takes directories as well as files 2021-09-17 18:56:57 +02:00
Tomáš Mládek cd76a36d99 adjust .gitlab-ci.yml to also build app, remove Makefile dep 2021-09-15 21:29:48 +02:00
Tomáš Mládek 71af6bb680 add base CI for Deno 2021-09-15 21:22:18 +02:00
Tomáš Mládek 6720eace7a add Makefile 2021-09-15 21:19:22 +02:00
Tomáš Mládek 0982857082 fix linter errors, reformat 2021-09-15 21:19:03 +02:00
Tomáš Mládek 1ba396bc7c make image loading work even in compiled version (?) 2021-09-15 21:16:01 +02:00
Tomáš Mládek a656d37d8c reformat cli/main.ts 2021-09-15 21:15:41 +02:00
Tomáš Mládek 14efcf03a1 unnecessary files, fix default extension 2021-09-15 20:56:40 +02:00
Tomáš Mládek b5a6b70c2b npx browserslist@latest --update-db 2021-09-15 19:14:58 +02:00
Tomáš Mládek 5734774366 autoformat, fix lint errors 2021-09-15 19:13:26 +02:00
Tomáš Mládek 054f9a9218 deno cli version (ugly) proof of concept 2021-09-15 19:09:44 +02:00
Tomáš Mládek 79f9108f34 autoformat 2021-09-11 23:51:22 +02:00
Tomáš Mládek de53e1b741 add CHANGELOG.md 2020-07-20 15:11:22 +02:00
Tomáš Mládek d319de86d9 version bump 2020-07-20 14:57:14 +02:00
Tomáš Mládek f54b61e236 add exclusion for shuffle & recursive modes 2020-07-20 14:57:05 +02:00
Tomáš Mládek c481d5739f version bump proper 2020-07-17 19:57:46 +02:00
Tomáš Mládek 89091dcd24 version bump 2020-07-17 19:14:52 +02:00
Tomáš Mládek 64fa7098a0 add recursion 2020-07-17 19:14:42 +02:00
Tomáš Mládek 7cf393af71 major refactor (add segments) in preparation for recursive collages 2020-07-17 16:11:40 +02:00
Tomáš Mládek 3866522404 version bump 2020-07-17 12:00:42 +02:00
Tomáš Mládek 5473d0c46a fix blending context 2020-07-17 12:00:33 +02:00
Tomáš Mládek ebb7bc537f version bump 2020-07-17 11:57:22 +02:00
Tomáš Mládek 4b102137f7 add blend mode, clear canvas before redraw, various improvements 2020-07-17 11:57:11 +02:00
Tomáš Mládek 1c3b9c7c46 Add LICENSE 2020-07-16 15:46:43 +00:00
25 changed files with 1871 additions and 310 deletions

4
.gitignore vendored
View File

@ -1,5 +1,6 @@
.DS_Store
node_modules
.npm
/dist
# local env files
@ -20,3 +21,6 @@ pnpm-debug.log*
*.njsproj
*.sln
*.sw?
# Build files
/kollagen

34
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,34 @@
stages:
- lint
- build
deno_lint:
stage: lint
image: denoland/deno:latest
script:
- cd cli
- deno lint
deno_build:
stage: build
image: denoland/deno:latest
script:
- cd cli
- deno compile --import-map ./import_map.json --allow-read --allow-write --unstable -o ../kollagen main.ts
artifacts:
paths:
- kollagen
app_build:
stage: build
image: node:lts
script:
- node --version && npm --version
- make app
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .npm
artifacts:
paths:
- dist

49
CHANGELOG.md Normal file
View File

@ -0,0 +1,49 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.2.0] - 2020-07-20
### Added
- Specific modes can now be excluded from rotation in shuffle & recursive modes
## [0.1.0] - 2020-07-17
### Added
- Recursive collage placement mode!
### Changed
- Refactored modes - they are no longer solely responsible for drawing, but can use common rendering functions through composition.
### Fixed
- Incorrect size calculation for "Irregular column" mode.
## [0.0.4] - 2020-07-17
### Fixed
- Blending mode now properly resets after using the "Blend" collage mode.
## [0.0.3] - 2020-07-17
### Added
- "Blend" collage mode - based on a random selection of compositing operations.
- LICENSE file.
## [0.0.2] - 2020-07-16
### Changed
- Split "Grid" mode into "Clean grid" and "Irregular grid" (because "clean cropping" config only really applied to this mode.)
## [0.0.1] - 2020-07-15
### Added
- First public version!
- Basic modes: Grid, (ir)regular row, (ir)regular column, concentric modes.
[Unreleased]: https://gitlab.com/tmladek/kollagen/-/compare/v0.2.0...master
[0.2.0]: https://gitlab.com/tmladek/kollagen/-/compare/v0.1.0...v0.2.0
[0.1.0]: https://gitlab.com/tmladek/kollagen/-/compare/v0.0.4...v0.1.0
[0.0.4]: https://gitlab.com/tmladek/kollagen/-/compare/v0.0.3...v0.0.4
[0.0.3]: https://gitlab.com/tmladek/kollagen/-/compare/v0.0.2...v0.0.3
[0.0.2]: https://gitlab.com/tmladek/kollagen/-/compare/v0.0.1...v0.0.2
[0.0.1]: https://gitlab.com/tmladek/kollagen/-/tags/v0.0.1

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Tomáš Mládek
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

21
Makefile Normal file
View File

@ -0,0 +1,21 @@
all: clean deno_lint deno_docker tgbot app
deno_lint:
cd cli && deno lint
deno: ./kollagen
./kollagen:
cd cli && deno compile --import-map ./import_map.json --allow-read --allow-write --unstable -o ../kollagen main.ts
deno_docker:
docker run --rm -v $$PWD:/app denoland/deno compile --import-map /app/cli/import_map.json --allow-read --allow-write --unstable -o /app/kollagen /app/cli/main.ts
app:
npm ci --cache .npm --prefer-offline && npm run build
tgbot: ./kollagen
docker build -t kollagen-bot -f tgbot/Dockerfile .
clean:
rm -rvf dist kollagen

96
cli/collages.ts Normal file
View File

@ -0,0 +1,96 @@
import { CollageModes } from "../src/common/collages.ts";
import {
init,
CanvasRenderingContext2D,
EmulatedCanvas2D,
Image,
} from "https://deno.land/x/canvas@v1.3.0/mod.ts";
import { CollageImage } from "../src/common/types.ts";
const canvasKit = await init();
export class ProxyImage implements CollageImage {
private filepath: string | null;
private _image: Image | undefined;
constructor(input: string | Image) {
if (typeof input === "string") {
this.filepath = input;
} else {
this.filepath = null;
this._image = input;
}
}
public get path(): string | null {
return this.filepath;
}
public get image(): Image {
if (!this._image) {
const image = canvasKit.MakeImageFromEncoded(
Deno.readFileSync(this.filepath!)
);
if (!image) {
throw Error(`Failed loading ${this.filepath}!`);
}
this._image = image;
}
return this._image;
}
public get width(): number {
return this.image.width();
}
public get height(): number {
return this.image.height();
}
}
declare module "https://deno.land/x/canvas@v1.3.0/mod.ts" {
interface HTMLCanvasElement {
width: number;
height: number;
getContext(x: "2d"): CanvasRenderingContext2D;
}
}
export class DenoCollageModes extends CollageModes<
CanvasRenderingContext2D,
ProxyImage,
EmulatedCanvas2D
> {
createCanvas(w: number, h: number): EmulatedCanvas2D {
const canvas = canvasKit.MakeCanvas(Math.round(w), Math.round(h));
if (!canvas) {
throw Error("Error initializing canvas.");
}
return canvas;
}
canvasToImage(canvas: EmulatedCanvas2D): PromiseLike<ProxyImage> {
const image = canvasKit.MakeImageFromEncoded(canvas.toBuffer());
if (!image) {
throw Error("Something went wrong converting canvas to image.");
}
return Promise.resolve(new ProxyImage(image));
}
drawImage(
ctx: CanvasRenderingContext2D,
image: ProxyImage,
sx: number,
sy: number,
sw: number,
sh: number,
dx: number,
dy: number,
dw: number,
dh: number
): void {
ctx.drawImage(image.image, sx, sy, sw, sh, dx, dy, dw, dh);
}
}
export const denoCollageModes = new DenoCollageModes();

6
cli/import_map.json Normal file
View File

@ -0,0 +1,6 @@
{
"imports": {
"@/common/types": "../src/common/types.ts",
"@/common/utils": "../src/common/utils.ts"
}
}

105
cli/main.ts Normal file
View File

@ -0,0 +1,105 @@
import { parse } from "https://deno.land/std@0.106.0/flags/mod.ts";
import { createCanvas } from "https://deno.land/x/canvas@v1.3.0/mod.ts";
import {
collageModeType,
DisplayCollageModeType,
displayCollageModeType,
} from "../src/common/collages.ts";
import { denoCollageModes, ProxyImage } from "./collages.ts";
import { CollageConfig } from "../src/common/types.ts";
import { choice, shuffle } from "../src/common/utils.ts";
import { walkSync } from "https://deno.land/std@0.107.0/fs/mod.ts";
import { parseCollageModes, parseDisplayCollageModes } from "./util.ts";
const args = parse(Deno.args, {
alias: {
w: "width",
h: "height",
o: "output",
m: "mode",
},
boolean: ["rr"],
default: {
w: 640,
h: 640,
include: "*.png, *.jpg",
output: "collage.png",
rl: 2,
rr: false,
},
});
if (args["mode"] === true) {
console.log(displayCollageModeType.join(", "));
Deno.exit(0);
}
const files: Set<string> = new Set();
const includeExtensions = Array.from(
String(args["include"]).matchAll(/\*\.([\w]+)/g)
).map(([_, group]) => group);
args["_"].forEach((arg) => {
arg = arg.toString();
if (Deno.statSync(arg).isDirectory) {
Array.from(
walkSync(arg, {
maxDepth: Infinity,
includeDirs: false,
includeFiles: true,
exts: includeExtensions.length ? includeExtensions : undefined,
})
).forEach((entry) => files.add(entry.path));
} else {
files.add(arg);
}
});
if (files.size < 2) {
console.error("kollagen needs at least 2 images to work.");
Deno.exit(1);
}
const shuffledFiles = shuffle(Array.from(files));
const images: ProxyImage[] = shuffledFiles.map((file) => new ProxyImage(file));
const allModeKeys = args["mode"]
? parseDisplayCollageModes(args["mode"])
: displayCollageModeType;
const canvas = createCanvas(args["width"], args["height"]);
const context = canvas.getContext("2d");
const collageConfig: CollageConfig = {
numImages: args["n"],
};
const modeKey: DisplayCollageModeType = choice(allModeKeys);
if (modeKey === "recursive") {
console.log(
`Creating a recursive collage, choosing from ${shuffledFiles.length} files...`
);
await denoCollageModes.recursiveDraw(context, images, {
modes: args["rm"] ? parseCollageModes(args["rm"]) : collageModeType,
repeat: args["rr"],
level: args["rl"],
});
} else {
const mode = denoCollageModes.modes[modeKey];
console.log(
`Creating a "${mode.name}" collage, choosing from ${shuffledFiles.length} files...`
);
const segments = mode.getSegments(context, collageConfig, images);
mode.place(context, images, segments);
console.log(
`Used: ${images
.slice(0, segments.length)
.map((img) => img.path)
.join(", ")}`
);
}
const output = args["output"];
console.log(`Saving to "${output}"...`);
await Deno.writeFile(output, canvas.toBuffer());

29
cli/util.ts Normal file
View File

@ -0,0 +1,29 @@
import { CollageModeType, DisplayCollageModeType, isCollageModeType, isDisplayCollageModeType } from "../src/common/collages.ts";
export function parseDisplayCollageModes(input: string): DisplayCollageModeType[] {
const result: DisplayCollageModeType[] = [];
input.split(",").map((m) => m.trim()).forEach((m) => {
if (isDisplayCollageModeType(m)) {
result.push(m);
} else {
throw Error(`"${m}" is not a valid collage mode.`);
}
});
return result;
}
export function parseCollageModes(input: string): CollageModeType[] {
const result: CollageModeType[] = [];
input.split(",").map((m) => m.trim()).forEach((m) => {
if (isCollageModeType(m)) {
result.push(m);
} else {
throw Error(`"${m}" is not a valid collage mode.`);
}
});
return result;
}

8
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "kollagen",
"version": "0.1.0",
"version": "0.2.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -2810,9 +2810,9 @@
}
},
"caniuse-lite": {
"version": "1.0.30001085",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001085.tgz",
"integrity": "sha512-x0YRFRE0pmOD90z+9Xk7jwO58p4feVNXP+U8kWV+Uo/HADyrgESlepzIkUqPgaXkpyceZU6siM1gsK7sHgplqA==",
"version": "1.0.30001257",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001257.tgz",
"integrity": "sha512-JN49KplOgHSXpIsVSF+LUyhD8PUp6xPpAXeRrrcBh4KBeP7W864jHn6RvzJgDlrReyeVjMFJL3PLpPvKIxlIHA==",
"dev": true
},
"case-sensitive-paths-webpack-plugin": {

View File

@ -1,6 +1,6 @@
{
"name": "kollagen",
"version": "0.0.2",
"version": "0.2.0",
"homepage": "https://gitlab.com/tmladek/kollagen",
"private": true,
"scripts": {

View File

@ -1,173 +1,14 @@
import {CollageMode} from "@/types";
import {randint, shuffle} from "@/utils";
import { CollageModes } from "./common/collages";
const collageModeType = ["clean_grid", "chaos_grid", "row", "irow", "col", "icol", "concentric_factor", "concentric_spaced"] as const;
export type CollageModeType = typeof collageModeType[number];
function cleanDraw(ctx: CanvasRenderingContext2D, image: ImageBitmap,
x: number, y: number, w: number, h: number) {
const scaleRatio = Math.max(w / image.width, h / image.height);
ctx.drawImage(
image,
image.width / 2 - w / scaleRatio / 2, image.height / 2 - h / scaleRatio / 2,
w / scaleRatio, h / scaleRatio,
x - w / 2, y - h / 2,
w, h
);
}
function getGridPoints(ctx: CanvasRenderingContext2D, idx: number) {
let x!: number, y!: number;
switch (idx) {
case 0:
x = ctx.canvas.width * .25;
y = ctx.canvas.height * .25;
break;
case 1:
x = ctx.canvas.width * .75;
y = ctx.canvas.height * .25;
break;
case 2:
x = ctx.canvas.width * .25;
y = ctx.canvas.height * .75;
break;
case 3:
x = ctx.canvas.width * .75;
y = ctx.canvas.height * .75;
break;
export default class BrowserCollageModes extends CollageModes<CanvasRenderingContext2D, ImageBitmap, any> {
drawImage(ctx: CanvasRenderingContext2D, image: ImageBitmap, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void {
ctx.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh);
}
return [x, y];
}
const modes: { [key in CollageModeType]: CollageMode } = {
"clean_grid": {
name: "Clean Grid",
minImages: 4,
forceConfig: {
numImages: 4
},
place: (ctx, images, config) => {
const quadrantSize = [ctx.canvas.width / 2, ctx.canvas.height / 2];
const selectedImages = shuffle(images).slice(0, 4);
selectedImages.forEach((image, idx) => {
const [x, y] = getGridPoints(ctx, idx);
cleanDraw(ctx, image, x, y, quadrantSize[0], quadrantSize[1]);
});
}
},
"chaos_grid": {
name: "Irregular Grid",
minImages: 4,
forceConfig: {
numImages: 4
},
place: (ctx, images, config) => {
const quadrantSize = [ctx.canvas.width / 2, ctx.canvas.height / 2];
const selectedImages = shuffle(images).slice(0, 4);
shuffle(selectedImages.map((image, idx) => [image, idx] as [ImageBitmap, number]))
.forEach(([image, idx]) => {
const [x, y] = getGridPoints(ctx, idx);
const scaleRatio = Math.max(quadrantSize[0] / image.width, quadrantSize[1] / image.height);
ctx.drawImage(image,
x - (image.width * scaleRatio / 2), y - (image.height * scaleRatio / 2),
image.width * scaleRatio, image.height * scaleRatio);
});
}
},
"row": {
name: "Regular Row",
minImages: 2,
place: (ctx, images, config) => {
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
const quadrantSize = [ctx.canvas.width / selectedImages.length, ctx.canvas.height];
selectedImages.forEach((image, idx) => {
const x = idx * quadrantSize[0] + quadrantSize[0] / 2;
const y = quadrantSize[1] / 2;
cleanDraw(ctx, image, x, y, quadrantSize[0], quadrantSize[1]);
});
}
},
"irow": {
name: "Irregular Row",
minImages: 2,
place: (ctx, images, config) => {
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
const quadrantSize = [ctx.canvas.width / selectedImages.length, ctx.canvas.height];
selectedImages.forEach((image, idx) => {
const x = idx * quadrantSize[0] + quadrantSize[0] / 2;
const y = quadrantSize[1] / 2;
const w = Math.min(ctx.canvas.width / 2,
quadrantSize[1] + Math.random() * (quadrantSize[1] - (quadrantSize[1] / image.height) * image.width));
cleanDraw(ctx, image, x, y, w, quadrantSize[1]);
});
}
},
"col": {
name: "Regular Column",
minImages: 2,
place: (ctx, images, config) => {
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
const quadrantSize = [ctx.canvas.width, ctx.canvas.height / selectedImages.length];
selectedImages.forEach((image, idx) => {
const x = quadrantSize[0] / 2;
const y = idx * quadrantSize[1] + quadrantSize[1] / 2;
cleanDraw(ctx, image, x, y, quadrantSize[0], quadrantSize[1]);
});
}
},
"icol": {
name: "Irregular Column",
minImages: 2,
place: (ctx, images, config) => {
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
const quadrantSize = [ctx.canvas.width, ctx.canvas.height / selectedImages.length];
selectedImages.forEach((image, idx) => {
const x = quadrantSize[0] / 2;
const y = idx * quadrantSize[1] + quadrantSize[1] / 2;
const h = Math.min(ctx.canvas.height / 2,
quadrantSize[0] + Math.random() * (quadrantSize[0] - (quadrantSize[0] / image.width) * image.height));
cleanDraw(ctx, image, x, y, quadrantSize[0], h);
});
},
},
"concentric_factor": {
name: "Constant factor concentric",
minImages: 2,
place: (ctx, images, config) => {
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
const x = ctx.canvas.width / 2;
const y = ctx.canvas.height / 2;
let factor: number;
if (Math.random() > .5) {
const factors = [1 / Math.sqrt(2), .5, .88];
factor = factors[Math.floor(Math.random() * factors.length)];
} else {
factor = 1 - (1 / selectedImages.length);
}
selectedImages.forEach((image, idx) => {
const ratio = Math.pow(factor, idx);
cleanDraw(ctx, image, x, y, ctx.canvas.width * ratio, ctx.canvas.height * ratio);
});
}
},
"concentric_spaced": {
name: "Equally spaced concentric",
minImages: 2,
place: (ctx, images, config) => {
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
const x = ctx.canvas.width / 2;
const y = ctx.canvas.height / 2;
selectedImages.forEach((image, idx) => {
cleanDraw(
ctx, image, x, y,
ctx.canvas.width - (ctx.canvas.width / selectedImages.length * idx),
ctx.canvas.height - (ctx.canvas.height / selectedImages.length * idx),
);
});
}
createCanvas(w: number, h: number) {
throw new Error("Method not implemented.");
}
};
export default modes;
canvasToImage(canvas: any): PromiseLike<ImageBitmap> {
throw new Error("Method not implemented.");
}
}

397
src/common/collages.ts Normal file
View File

@ -0,0 +1,397 @@
import {
CollageCanvas,
CollageConfig,
CollageContext,
CollageImage,
CollageMode,
Segment,
} from "@/common/types";
import { choice, randint, range, shuffle } from "@/common/utils";
export const collageModeType = [
"clean_grid",
"chaos_grid",
"row",
"irow",
"col",
"icol",
"concentric_factor",
"concentric_spaced",
"blend",
] as const;
export type CollageModeType = typeof collageModeType[number];
export function isCollageModeType(value: string): value is CollageModeType {
return (collageModeType as readonly string[]).includes(value);
}
export const displayCollageModeType = [
...collageModeType,
"recursive",
] as const;
export type DisplayCollageModeType = typeof displayCollageModeType[number];
export function isDisplayCollageModeType(
value: string
): value is DisplayCollageModeType {
return (displayCollageModeType as readonly string[]).includes(value);
}
export interface RecursiveCollageConfig {
level: number;
repeat: boolean;
modes: readonly CollageModeType[];
}
export abstract class CollageModes<
C extends CollageContext,
I extends CollageImage,
CS extends CollageCanvas
> {
readonly modes: { [key in CollageModeType]: CollageMode<C, I> } = {
clean_grid: {
name: "Clean Grid",
minImages: 4,
forceConfig: {
numImages: 4,
},
getSegments: this.getGridSegments,
place: this.cleanPlace.bind(this),
},
chaos_grid: {
name: "Irregular Grid",
minImages: 4,
forceConfig: {
numImages: 4,
},
getSegments: this.getGridSegments,
place: (ctx, images, segments) => {
const shuffledImages = shuffle(images);
shuffle(
segments.map((segment, idx) => [segment, idx] as [Segment, number])
).forEach(([segment, idx]) => {
const image = shuffledImages[idx];
const scaleRatio = Math.max(
segment.w / image.width,
segment.h / image.height
);
this.drawImage(
ctx,
image,
0,
0,
image.width,
image.height,
segment.x - (image.width * scaleRatio) / 2,
segment.y - (image.height * scaleRatio) / 2,
image.width * scaleRatio,
image.height * scaleRatio
);
});
},
},
row: {
name: "Regular Row",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(
images?.length || Infinity,
config?.numImages || randint(4) + 2
);
const segmentSize = [ctx.canvas.width / numImages, ctx.canvas.height];
return range(numImages).map((idx) => {
return {
x: idx * segmentSize[0] + segmentSize[0] / 2,
y: segmentSize[1] / 2,
w: segmentSize[0],
h: segmentSize[1],
};
});
},
place: this.cleanPlace.bind(this),
},
irow: {
name: "Irregular Row",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(
images?.length || Infinity,
config?.numImages || randint(4) + 2
);
const segmentSize = [ctx.canvas.width / numImages, ctx.canvas.height];
return range(numImages).map((idx) => {
const irregularWidth = images
? segmentSize[0] +
Math.random() *
((segmentSize[1] / images[idx].height) * images[idx].width -
segmentSize[0])
: segmentSize[0] + Math.random() * segmentSize[0] * 0.5;
return {
x: idx * segmentSize[0] + segmentSize[0] / 2,
y: segmentSize[1] / 2,
w: Math.min(ctx.canvas.width / 2, irregularWidth),
h: segmentSize[1],
};
});
},
place: this.cleanPlace.bind(this),
},
col: {
name: "Regular Column",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(
images?.length || Infinity,
config?.numImages || randint(4) + 2
);
const segmentSize = [ctx.canvas.width, ctx.canvas.height / numImages];
return range(numImages).map((idx) => {
return {
x: segmentSize[0] / 2,
y: idx * segmentSize[1] + segmentSize[1] / 2,
w: segmentSize[0],
h: segmentSize[1],
};
});
},
place: this.cleanPlace.bind(this),
},
icol: {
name: "Irregular Column",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(
images?.length || Infinity,
config?.numImages || randint(4) + 2
);
const segmentSize = [ctx.canvas.width, ctx.canvas.height / numImages];
return range(numImages).map((idx) => {
const irregularHeight = images
? segmentSize[1] +
Math.random() *
((segmentSize[0] / images[idx].width) * images[idx].height -
segmentSize[1])
: segmentSize[1] + Math.random() * segmentSize[1] * 0.5;
return {
x: segmentSize[0] / 2,
y: idx * segmentSize[1] + segmentSize[1] / 2,
w: segmentSize[0],
h: Math.min(ctx.canvas.height / 2, irregularHeight),
};
});
},
place: this.cleanPlace.bind(this),
},
concentric_factor: {
name: "Constant factor concentric",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(
images?.length || Infinity,
config?.numImages || randint(4) + 2
);
let factor: number;
if (Math.random() > 0.5) {
factor = choice([1 / Math.sqrt(2), 0.5, 0.88]);
} else {
factor = 1 - 1 / numImages;
}
return range(numImages).map((idx) => {
const ratio = Math.pow(factor, idx);
return {
x: ctx.canvas.width / 2,
y: ctx.canvas.height / 2,
w: ctx.canvas.width * ratio,
h: ctx.canvas.height * ratio,
};
});
},
place: this.cleanPlace.bind(this),
},
concentric_spaced: {
name: "Equally spaced concentric",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(
images?.length || Infinity,
config?.numImages || randint(2) + 2
);
return range(numImages).map((idx) => {
return {
x: ctx.canvas.width / 2,
y: ctx.canvas.height / 2,
w: ctx.canvas.width - (ctx.canvas.width / numImages) * idx,
h: ctx.canvas.height - (ctx.canvas.height / numImages) * idx,
};
});
},
place: this.cleanPlace.bind(this),
},
blend: {
name: "Blending",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(
images?.length || Infinity,
config?.numImages || randint(2) + 2
);
return range(numImages).map((_) => {
return {
x: ctx.canvas.width / 2,
y: ctx.canvas.height / 2,
w: ctx.canvas.width,
h: ctx.canvas.height,
};
});
},
place: (ctx, images, segments) => {
ctx.globalCompositeOperation = choice([
"difference",
"saturation",
"soft-light",
"overlay",
]);
this.cleanPlace(ctx, images, segments);
},
},
};
public async recursiveDraw(
context: C,
images: I[],
recursiveConfig: RecursiveCollageConfig
) {
const localImages = images.concat();
const rootSegment: Segment = {
x: 0,
y: 0,
w: context.canvas.width,
h: context.canvas.height,
};
const processSegment = async (
segment: Segment,
level: number
): Promise<I> => {
// console.debug(segment, level);
if (segment === rootSegment || level <= recursiveConfig.level - 1) {
const canvas = this.createCanvas(segment.w, segment.h);
const modeKey = choice(recursiveConfig.modes);
// console.debug(modeKey);
const mode = this.modes[modeKey];
const ctx = canvas.getContext("2d") as C;
const segments = mode.getSegments(ctx);
// console.debug(segments);
const bitmaps = await Promise.all(
segments.map((segment: Segment) => processSegment(segment, level + 1))
);
mode.place(ctx, bitmaps, segments);
return await this.canvasToImage(canvas);
} else {
if (recursiveConfig.repeat) {
return choice(localImages);
} else {
if (localImages.length > 0) {
return localImages.pop() as I;
} else {
throw "RAN OUT OF IMAGES";
}
}
}
};
const result = await processSegment(rootSegment, 0);
this.drawImage(
context,
result,
0,
0,
result.width,
result.height,
0,
0,
context.canvas.width,
context.canvas.height
);
}
private cleanDraw(
ctx: C,
image: I,
x: number,
y: number,
w: number,
h: number
) {
const scaleRatio = Math.max(w / image.width, h / image.height);
this.drawImage(
ctx,
image,
image.width / 2 - w / scaleRatio / 2,
image.height / 2 - h / scaleRatio / 2,
w / scaleRatio,
h / scaleRatio,
x - w / 2,
y - h / 2,
w,
h
);
}
private cleanPlace(ctx: C, images: I[], segments: Segment[]) {
segments.forEach((segment, idx) => {
this.cleanDraw(
ctx,
images[idx],
segment.x,
segment.y,
segment.w,
segment.h
);
});
}
private getGridSegments(ctx: C, config?: CollageConfig) {
return [0, 1, 2, 3].map((idx) => {
let x!: number, y!: number;
switch (idx) {
case 0:
x = ctx.canvas.width * 0.25;
y = ctx.canvas.height * 0.25;
break;
case 1:
x = ctx.canvas.width * 0.75;
y = ctx.canvas.height * 0.25;
break;
case 2:
x = ctx.canvas.width * 0.25;
y = ctx.canvas.height * 0.75;
break;
case 3:
x = ctx.canvas.width * 0.75;
y = ctx.canvas.height * 0.75;
break;
}
return {
x,
y,
w: ctx.canvas.width / 2,
h: ctx.canvas.height / 2,
};
});
}
abstract createCanvas(w: number, h: number): CS;
abstract canvasToImage(canvas: CS): PromiseLike<I>;
abstract drawImage(
ctx: C,
image: I,
sx: number,
sy: number,
sw: number,
sh: number,
dx: number,
dy: number,
dw: number,
dh: number
): void;
}

35
src/common/types.ts Normal file
View File

@ -0,0 +1,35 @@
export interface CollageMode<C extends CollageContext, I extends CollageImage> {
name: string;
minImages: number;
getSegments: (ctx: C, config?: CollageConfig, images?: I[]) => Segment[];
place: (ctx: C, images: I[], segments: Segment[]) => void;
forceConfig?: CollageConfig;
}
export interface CollageConfig {
numImages?: number;
}
export interface Segment {
x: number;
y: number;
w: number;
h: number;
}
export interface CollageContext {
globalCompositeOperation: string;
canvas: CollageCanvas
}
export interface CollageCanvas {
width: number;
height: number;
getContext: (x: '2d') => CollageContext | null
}
export interface CollageImage {
width: number;
height: number;
}

View File

@ -17,3 +17,11 @@ export function shuffle<T>(a: T[]): T[] {
export function randint(n: number) {
return Math.floor(Math.random() * n);
}
export function choice<T>(arr: readonly T[]): T {
return arr[randint(arr.length)];
}
export function range(n: number): number[] {
return Array.from({length: n}, (x, i) => i);
}

View File

@ -1,190 +1,397 @@
<template>
<div class="collage">
<div class="collage">
<div class="canvas">
<canvas id="canvas" ref="canvas" :width="canvasSize.width" :height="canvasSize.height"></canvas>
<div class="canvas-size">
<label>
Width:
<input type="number" step="16" min="128" v-model="canvasSize.width">
</label>
<label>
Height:
<input type="number" step="16" min="128" v-model="canvasSize.height">
</label>
</div>
<canvas
id="canvas"
ref="canvas"
:width="canvasSize.width"
:height="canvasSize.height"
></canvas>
<div class="canvas-size">
<label>
Width:
<input type="number" step="16" min="128" v-model="canvasSize.width" />
</label>
<label>
Height:
<input
type="number"
step="16"
min="128"
v-model="canvasSize.height"
/>
</label>
</div>
</div>
<div class="controls">
<div class="modes">
<label v-for="(mode, idx) in modes"
:class="{disabled: images.length < mode.minImages,
selected: idx === currentModeType,
lastActive: idx === lastActiveModeType}">
{{mode.name}}
<input type="radio" :value="idx" v-model="currentModeType">
</label>
</div>
<button :disabled="images.length < currentMode.minImages" @click="renderCollage">REPAINT</button>
<hr>
<div class="config">
<label class="config-numimages">
#N of images:
<input type="number" :min="currentMode.minImages" :max="images.length"
placeholder="RND"
:disabled="Object.keys(forceConfig).includes('numImages')"
v-model="forceConfig.numImages || collageConfig.numImages">
</label>
</div>
<div class="modes">
<ul class="modes-list">
<li
v-for="(mode, idx) in modes.modes"
:class="[
'mode',
{
disabled: images.length < mode.minImages,
excluded: excludedModes.includes(idx),
selected: idx === currentModeType,
lastActive: lastActiveModeTypes.includes(idx),
},
]"
:key="idx"
>
<label class="handle"
>{{ mode.name
}}<input type="radio" :value="idx" v-model="currentModeType"
/></label>
<span
class="mode-plus"
v-if="
['recursive', 'shuffle'].includes(currentModeType) &&
images.length >= minImages
"
>
<span class="separator"></span>
<label
>{{ excludedModes.includes(idx) ? "o" : "x"
}}<input type="checkbox" :value="idx" v-model="excludedModes"
/></label>
</span>
</li>
</ul>
<hr />
<label
:class="{
disabled: images.length < minImages,
selected: 'shuffle' === currentModeType,
}"
>
!SHUFFLE ALL!
<input type="radio" value="shuffle" v-model="currentModeType" />
</label>
<label
:class="{
disabled: images.length < minImages,
selected: 'recursive' === currentModeType,
}"
>
#RECURSIVE#
<input type="radio" value="recursive" v-model="currentModeType" />
</label>
</div>
<button :disabled="images.length < minImages" @click="renderCollage">
REPAINT
</button>
<hr />
<div class="config">
<template v-if="currentModeType !== 'recursive'">
<label class="config-numimages">
#N of images:
<input
type="number"
:min="minImages"
:max="images.length"
placeholder="RND"
:disabled="Object.keys(forceConfig).includes('numImages')"
:value="forceConfig.numImages || collageConfig.numImages"
@input="(n) => (collageConfig.numImages = n)"
/>
</label>
</template>
<template v-else>
<label>
Recursion levels:
<input
type="number"
:min="1"
:max="10"
v-model="recursiveConfig.level"
/>
</label>
<label>
<input type="checkbox" v-model="recursiveConfig.repeat" />
Repeat images?
</label>
</template>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import {Component, Prop, Vue, Watch} from "vue-property-decorator";
import collageModes, {CollageModeType} from "../collages";
import {CollageConfig, CollageMode} from "@/types";
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import BrowserCollageModes from "../collages";
import { CollageModeType } from "../common/collages";
import { CollageConfig, CollageMode, Segment } from "../common/types";
import { choice, shuffle } from "../common/utils";
type DisplayCollageModeType = CollageModeType | & "shuffle";
type DisplayCollageModeType = CollageModeType | "shuffle" | "recursive";
@Component
export default class Collage extends Vue {
@Prop({required: true}) private images!: ImageBitmap[];
private context!: CanvasRenderingContext2D;
private canvasSize = {
width: 640,
height: 640
};
private collageConfig: CollageConfig = {
numImages: undefined
};
private currentModeType: DisplayCollageModeType = "shuffle";
private lastActiveModeType: DisplayCollageModeType | null = null;
private modes: { [key in DisplayCollageModeType]: CollageMode } = {
...collageModes,
"shuffle": {
name: "Shuffle all!",
minImages: Math.min(...Object.values(collageModes).map(m => m.minImages)),
place: (ctx, images, config) => {
const permissibleModeKeys = Object.keys(collageModes)
.filter(k => collageModes[k as CollageModeType].minImages <= images.length) as CollageModeType[];
const randomModeType = permissibleModeKeys[Math.floor(Math.random() * permissibleModeKeys.length)];
const randomMode = collageModes[randomModeType];
this.setLastActiveModeType(randomModeType);
randomMode.place(ctx, images, config);
@Prop({ required: true }) private images!: ImageBitmap[];
private context!: CanvasRenderingContext2D;
private canvasSize = {
width: 640,
height: 640,
};
private collageConfig: CollageConfig = {
numImages: undefined,
};
private recursiveConfig = {
level: 2,
repeat: true,
};
private currentModeType: DisplayCollageModeType = "shuffle";
private lastActiveModeTypes: CollageModeType[] = [];
private excludedModes: CollageModeType[] = [];
private modes = new BrowserCollageModes();
private get minImages() {
if (
this.currentModeType === "shuffle" ||
this.currentModeType === "recursive"
) {
return Math.min(
...Object.values(this.modes.modes).map((mode) => mode.minImages)
);
} else {
return this.modes.modes[this.currentModeType].minImages;
}
}
private get lastMode() {
if (this.lastActiveModeTypes.length === 1) {
return this.modes.modes[this.lastActiveModeTypes[0]];
}
}
private get forceConfig() {
return this.lastMode ? this.lastMode.forceConfig || {} : {};
}
private mounted() {
const canvas = this.$refs.canvas as HTMLCanvasElement;
this.context = canvas.getContext("2d") as CanvasRenderingContext2D;
}
@Watch("images")
@Watch("currentModeType")
@Watch("collageConfig", { deep: true })
@Watch("recursiveConfig", { deep: true })
private renderCollage() {
if (this.images.length >= this.minImages) {
this.reset();
const permissibleModeKeys = (
Object.keys(this.modes.modes) as CollageModeType[]
).filter(
(k) =>
!this.excludedModes.includes(k) &&
this.modes.modes[k].minImages <= this.images.length
);
if (this.currentModeType !== "recursive") {
let mode: CollageMode<any, any>;
if (this.currentModeType === "shuffle") {
const randomModeType = choice(permissibleModeKeys);
this.lastActiveModeTypes = [randomModeType];
mode = this.modes.modes[randomModeType];
} else {
this.lastActiveModeTypes = [this.currentModeType];
mode = this.modes.modes[this.currentModeType];
}
const shuffledImages = shuffle(this.images);
const segments = mode.getSegments(
this.context,
this.collageConfig,
shuffledImages
);
mode.place(this.context, shuffledImages, segments);
} else {
this.lastActiveModeTypes = [];
const shuffledImages = shuffle(this.images);
const rootSegment: Segment = {
x: 0,
y: 0,
w: this.context.canvas.width,
h: this.context.canvas.height,
};
const processSegment = async (
segment: Segment,
level: number
): Promise<ImageBitmap> => {
console.debug(segment, level);
if (
segment === rootSegment ||
level <= this.recursiveConfig.level - 1
) {
let canvas = document.createElement("canvas");
canvas.width = segment.w;
canvas.height = segment.h;
let modeKey = choice(permissibleModeKeys);
console.debug(modeKey);
this.lastActiveModeTypes.push(modeKey);
let mode = this.modes.modes[modeKey];
let ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
let segments = mode.getSegments(ctx);
console.debug(segments);
let bitmaps = await Promise.all(
segments.map((segment) => processSegment(segment, level + 1))
);
mode.place(ctx, bitmaps, segments);
return await createImageBitmap(canvas);
} else {
if (this.recursiveConfig.repeat) {
return choice(shuffledImages);
} else {
if (shuffledImages.length > 0) {
return shuffledImages.pop() as ImageBitmap;
} else {
throw "RAN OUT OF IMAGES";
}
}
}
};
// wtf vue?
private setLastActiveModeType(lastActiveModeType: any) {
this.lastActiveModeType = lastActiveModeType;
}
};
processSegment(rootSegment, 0)
.then((finalCollage) => {
console.debug(finalCollage);
this.context.drawImage(finalCollage, 0, 0);
})
.catch((error) => {
alert(error);
});
}
}
}
private get currentMode() {
return this.modes[this.currentModeType];
}
@Watch("canvasSize", { deep: true })
private onCanvasSizeChange() {
this.$nextTick(() => {
this.renderCollage();
});
}
private get lastMode() {
return this.lastActiveModeType ? this.modes[this.lastActiveModeType] : undefined;
}
private get forceConfig() {
return this.lastMode ? this.lastMode.forceConfig || {} : {};
}
private mounted() {
const canvas = (this.$refs.canvas as HTMLCanvasElement);
this.context = canvas.getContext("2d") as CanvasRenderingContext2D;
}
@Watch("images")
@Watch("currentMode")
@Watch("collageConfig", {deep: true})
private renderCollage() {
if (this.images.length >= this.currentMode.minImages) {
this.lastActiveModeType = this.currentModeType;
this.currentMode.place(this.context, this.images, this.collageConfig);
}
}
@Watch("canvasSize", {deep: true})
private onCanvasSizeChange() {
this.$nextTick(() => {
this.renderCollage();
});
}
private reset() {
this.context.globalCompositeOperation = "source-over";
const canvas = this.$refs.canvas as HTMLCanvasElement;
this.context.clearRect(0, 0, canvas.width, canvas.height);
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<!--suppress CssUnusedSymbol -->
<style scoped>
.collage {
margin: 2rem;
display: flex;
justify-content: space-evenly;
align-items: center;
margin: 2rem;
display: flex;
justify-content: space-evenly;
align-items: center;
}
.canvas {
display: flex;
flex-direction: column;
align-items: center;
display: flex;
flex-direction: column;
align-items: center;
}
#canvas {
border: 1px solid black;
border: 1px solid black;
}
.canvas-size {
margin: 1rem;
margin: 1rem;
}
.controls button {
width: 100%;
width: 100%;
}
.modes {
display: flex;
flex-direction: column;
align-items: center;
display: flex;
flex-direction: column;
align-items: center;
}
.modes-list {
list-style: none;
padding: 0;
}
.mode {
display: flex;
justify-content: center;
align-items: center;
user-select: none;
}
.separator {
border: 1px solid gray;
width: 0;
margin: 0 0.5em;
align-self: stretch;
}
.mode .mode-plus,
.mode .mode-plus label {
color: gray;
font-size: 14px;
}
.mode-plus {
display: flex;
align-items: center;
}
.controls .modes hr {
margin-top: 0.5rem;
width: 100%;
color: lightgray;
}
.controls label {
font-size: 14pt;
cursor: pointer;
margin: .25rem;
font-size: 14pt;
cursor: pointer;
margin: 0.25rem;
}
.modes input {
display: none;
display: none;
}
.controls button, .controls hr, .controls .config {
margin-top: 1rem;
.controls button,
.controls hr,
.controls .config {
margin-top: 1rem;
}
.controls .config {
user-select: none;
user-select: none;
}
.config label {
display: block;
display: block;
}
.config-numimages input {
width: 4em;
width: 4em;
}
.disabled {
color: gray;
pointer-events: none;
color: gray;
pointer-events: none;
}
.excluded .handle {
color: gray;
text-decoration: line-through;
}
.selected {
font-weight: bold;
font-weight: bold;
}
.lastActive {
text-decoration: underline;
.lastActive .handle {
text-decoration: underline;
}
</style>

10
src/types.d.ts vendored
View File

@ -1,10 +0,0 @@
export interface CollageMode {
name: string;
minImages: number;
place: (ctx: CanvasRenderingContext2D, images: ImageBitmap[], config: CollageConfig) => void;
forceConfig?: CollageConfig;
}
export interface CollageConfig {
numImages?: number;
}

22
tgbot/Dockerfile Normal file
View File

@ -0,0 +1,22 @@
FROM python:3-slim
ENV PYTHONFAULTHANDLER=1 \
PYTHONUNBUFFERED=1 \
PYTHONHASHSEED=random \
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100
RUN pip install poetry
WORKDIR /app
COPY tgbot/poetry.lock tgbot/pyproject.toml /app/
RUN poetry config virtualenvs.create false \
&& poetry install --no-dev --no-interaction --no-ansi
COPY /tgbot/ /app/
COPY kollagen /usr/bin/kollagen
CMD python kollagen-bot/main.py

0
tgbot/README.rst Normal file
View File

BIN
tgbot/kollagen Executable file

Binary file not shown.

View File

@ -0,0 +1 @@
__version__ = '0.1.0'

270
tgbot/kollagen-bot/main.py Normal file
View File

@ -0,0 +1,270 @@
import logging
import os
import subprocess
from typing import List, Optional
from tempfile import NamedTemporaryFile
from telegram import Update
from telegram.ext import (
Updater,
CommandHandler,
DictPersistence,
CallbackContext,
)
from telegram.parsemode import ParseMode
from parser import SafeArgumentParser, safe_str
class KollagenBot:
def __init__(
self, tg_token: str, kollagen_path: str, base_dir: Optional[str]
) -> None:
self.logger = logging.getLogger("kollagen")
self.kollagen_path = kollagen_path
self.base_dir = os.path.abspath(base_dir) if base_dir else None
self._init_parser()
self.updater = Updater(
tg_token, persistence=DictPersistence(user_data_json="{}")
)
dispatcher = self.updater.dispatcher
dispatcher.add_handler(CommandHandler("start", self.tg_start))
dispatcher.add_handler(CommandHandler("help", self.tg_help))
dispatcher.add_handler(CommandHandler("generate", self.tg_generate))
dispatcher.add_handler(CommandHandler("g", self.tg_generate))
dispatcher.add_handler(CommandHandler("regenerate", self.tg_regenerate))
dispatcher.add_handler(CommandHandler("r", self.tg_regenerate))
dispatcher.add_error_handler(self.tg_error)
def _init_parser(self):
parser = SafeArgumentParser(prog="/generate", add_help=False)
parser.add_argument(
"directories",
metavar="path",
type=safe_str,
nargs="*",
default=[self.base_dir] if self.base_dir else [],
help="Directories or files to process. By default, the entire base directory is processed.",
)
parser.add_argument(
"-m",
dest="mode",
metavar="mode",
type=safe_str,
nargs="?",
const=True,
help=f"Collage modes to use. By default, one is chosen at random. Multiple modes can be specified, separated by commas. When no value is specified, all modes are listed.",
)
parser.add_argument(
"-n",
dest="num_images",
metavar="N",
type=int,
help=f"How many images to use in a single collage. Random (or collage-dependant) by default.",
)
parser.add_argument(
"-w",
dest="width",
type=int,
default=640,
help=f"Width of resulting output (in px). 640px by default.",
)
parser.add_argument(
"-h",
dest="height",
type=int,
default=640,
help=f"Height of resulting output (in px). 640px by default.",
)
parser.add_argument(
"--rm",
dest="recursive_modes",
type=safe_str,
help=f"Collage modes (comma-separated) to use in a recursive collage. All by default.",
)
parser.add_argument(
"--rl",
dest="recursive_level",
type=int,
default=2,
help=f"Level/depth of recursive collage. 2 by default.",
)
parser.add_argument(
"--rr",
dest="recursive_repeat",
action="store_true",
help=f"Allow repeating images in (different levels of) recursive collages. False by default.",
)
self.parser = parser
def _get_modes(self):
modes = subprocess.run(
[self.kollagen_path, "-m"], check=True, capture_output=True
)
return modes.stdout.decode("utf-8").strip().split(", ")
def tg_start(self, update: Update, context: CallbackContext):
update.message.reply_text("Hi! I make random collages. Check out https://gitlab.com/tmladek/kollagen and /help. Here's one to get you started:")
self._process([], update)
def tg_generate(self, update: Update, context: CallbackContext):
cmd_line = update.message.text.split(" ")[1:]
success = self._process(cmd_line, update)
if success and context.user_data is not None:
context.user_data["last_cmd_line"] = cmd_line
def tg_regenerate(self, update: Update, context: CallbackContext):
if context.user_data and context.user_data.get("last_cmd_line"):
self._process(context.user_data["last_cmd_line"], update)
else:
update.message.reply_text("No previous command to regenerate!")
def _process(self, cmd_line: List[str], update: Update):
self.logger.info(
f"Generating from {update.effective_user}, with cmd_line: `{cmd_line}`"
)
args = self.parser.parse_args(cmd_line)
if args.mode is True:
update.message.reply_text(
f"Available modes: {', '.join(self._get_modes())}"
)
return False
directories = []
for dir in args.directories:
for possible in [
os.path.join(self.base_dir or "./", dir),
os.path.join(self.base_dir or "./", dir.upper()),
]:
if os.path.exists(possible):
directories.append(possible)
break
else:
raise FileNotFoundError(f'"{dir}" does not exist.')
mode = ["-m", args.mode] if args.mode else []
num_images = ["-n", str(args.num_images)] if args.num_images else []
recursive_level = (
["--rl", str(args.recursive_level)] if args.recursive_level else []
)
recursive_repeat = ["--rr"] if args.recursive_repeat else []
recursive_modes = (
["--rm", str(args.recursive_modes)] if args.recursive_modes else []
)
with NamedTemporaryFile(suffix=".png") as ntf:
shell_cmd_line = [
self.kollagen_path,
*directories,
"-w",
str(args.width),
"-h",
str(args.height),
*mode,
*num_images,
*recursive_level,
*recursive_repeat,
*recursive_modes,
"-o",
ntf.name,
]
self.logger.debug(f"Running: " + str(shell_cmd_line))
result = subprocess.run(
shell_cmd_line,
check=True,
capture_output=True,
timeout=60,
env={
'NO_COLOR': "1"
}
)
ntf.seek(0)
used_line = next(
(
line
for line in result.stdout.decode("utf-8").splitlines()
if line.startswith("Used: ")
),
"",
).replace(f"{self.base_dir}/" if self.base_dir else "", "")
caption = ""
caption += (
f"`{' '.join(['/generate', *cmd_line])}`\n" if len(cmd_line) else ""
)
caption += used_line.replace("_", "\\_")
caption = caption[:200]
update.message.reply_photo(
ntf,
caption=caption,
parse_mode=ParseMode.MARKDOWN,
)
return True
def tg_help(self, update: Update, context: CallbackContext):
update.message.reply_text(
f"```{self.parser.format_help()}```", parse_mode=ParseMode.MARKDOWN
)
def tg_error(self, update: object, context: CallbackContext) -> None:
self.logger.error(
msg="Exception while handling an update:", exc_info=context.error
)
if isinstance(update, Update):
if isinstance(context.error, subprocess.CalledProcessError):
error_display = context.error.stderr.decode('utf-8')
else:
error_display = str(context.error)
error_display = error_display[:2500]
update.message.reply_text(
f"Something is fucked!\n{error_display}"
)
def start_idle(self):
self.updater.start_polling()
self.updater.idle()
def main() -> None:
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
level=getattr(logging, os.getenv("LOG_LEVEL", "info").upper()),
)
tg_token = os.getenv("TG_TOKEN")
if not tg_token:
logging.error("TG_TOKEN is required.")
exit(1)
if env_kollagen_path := os.getenv("KOLLAGEN_PATH"):
if os.path.exists(env_kollagen_path):
kollagen_path = env_kollagen_path
else:
logging.error(f"kollagen not found! {env_kollagen_path} does not exist.")
exit(1)
else:
which = subprocess.run(["which", "kollagen"], capture_output=True)
try:
which.check_returncode()
kollagen_path = which.stdout.decode("utf-8").strip()
except subprocess.CalledProcessError:
logging.error(
"kollagen not found! KOLLAGEN_PATH not specified and `kollagen` isn't in $PATH."
)
exit(1)
bot = KollagenBot(tg_token, kollagen_path, os.getenv("BASE_DIR"))
bot.start_idle()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,16 @@
import argparse
import re
class ArgumentParserError(RuntimeError):
pass
class SafeArgumentParser(argparse.ArgumentParser):
def error(self, message):
raise ArgumentParserError(message)
def safe_str(val: str):
if re.findall(r'[^\w,]', val):
raise RuntimeError("No special characters in arguments allowed!")
return val

393
tgbot/poetry.lock generated Normal file
View File

@ -0,0 +1,393 @@
[[package]]
name = "apscheduler"
version = "3.6.3"
description = "In-process task scheduler with Cron-like capabilities"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
pytz = "*"
six = ">=1.4.0"
tzlocal = ">=1.2"
[package.extras]
asyncio = ["trollius"]
doc = ["sphinx", "sphinx-rtd-theme"]
gevent = ["gevent"]
mongodb = ["pymongo (>=2.8)"]
redis = ["redis (>=3.0)"]
rethinkdb = ["rethinkdb (>=2.4.0)"]
sqlalchemy = ["sqlalchemy (>=0.8)"]
testing = ["pytest", "pytest-cov", "pytest-tornado5", "mock", "pytest-asyncio (<0.6)", "pytest-asyncio"]
tornado = ["tornado (>=4.3)"]
twisted = ["twisted"]
zookeeper = ["kazoo"]
[[package]]
name = "backports.zoneinfo"
version = "0.2.1"
description = "Backport of the standard library zoneinfo module"
category = "main"
optional = false
python-versions = ">=3.6"
[package.extras]
tzdata = ["tzdata"]
[[package]]
name = "black"
version = "21.9b0"
description = "The uncompromising code formatter."
category = "dev"
optional = false
python-versions = ">=3.6.2"
[package.dependencies]
click = ">=7.1.2"
mypy-extensions = ">=0.4.3"
pathspec = ">=0.9.0,<1"
platformdirs = ">=2"
regex = ">=2020.1.8"
tomli = ">=0.2.6,<2.0.0"
typing-extensions = [
{version = ">=3.10.0.0", markers = "python_version < \"3.10\""},
{version = "!=3.10.0.1", markers = "python_version >= \"3.10\""},
]
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
python2 = ["typed-ast (>=1.4.2)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "cachetools"
version = "4.2.2"
description = "Extensible memoizing collections and decorators"
category = "main"
optional = false
python-versions = "~=3.5"
[[package]]
name = "certifi"
version = "2021.5.30"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "click"
version = "8.0.1"
description = "Composable command line interface toolkit"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.4"
description = "Cross-platform colored terminal text."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "mypy-extensions"
version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "pathspec"
version = "0.9.0"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[[package]]
name = "platformdirs"
version = "2.3.0"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.extras]
docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"]
test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
[[package]]
name = "python-telegram-bot"
version = "13.7"
description = "We have made you a wrapper you can't refuse"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
APScheduler = "3.6.3"
cachetools = "4.2.2"
certifi = "*"
pytz = ">=2018.6"
tornado = ">=6.1"
[package.extras]
json = ["ujson"]
passport = ["cryptography (!=3.4,!=3.4.1,!=3.4.2,!=3.4.3)"]
socks = ["pysocks"]
[[package]]
name = "pytz"
version = "2021.1"
description = "World timezone definitions, modern and historical"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "regex"
version = "2021.8.28"
description = "Alternative regular expression module, to replace re."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "tomli"
version = "1.2.1"
description = "A lil' TOML parser"
category = "dev"
optional = false
python-versions = ">=3.6"
[[package]]
name = "tornado"
version = "6.1"
description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
category = "main"
optional = false
python-versions = ">= 3.5"
[[package]]
name = "typing-extensions"
version = "3.10.0.2"
description = "Backported and Experimental Type Hints for Python 3.5+"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "tzdata"
version = "2021.1"
description = "Provider of IANA time zone data"
category = "main"
optional = false
python-versions = ">=2"
[[package]]
name = "tzlocal"
version = "3.0"
description = "tzinfo object for the local timezone"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""}
tzdata = {version = "*", markers = "platform_system == \"Windows\""}
[package.extras]
test = ["pytest-mock (>=3.3)", "pytest (>=4.3)"]
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "a1390250d2dc7c6ca705c53cebcec951a6dd79717bb0f902b0177b307fac8bc3"
[metadata.files]
apscheduler = [
{file = "APScheduler-3.6.3-py2.py3-none-any.whl", hash = "sha256:e8b1ecdb4c7cb2818913f766d5898183c7cb8936680710a4d3a966e02262e526"},
{file = "APScheduler-3.6.3.tar.gz", hash = "sha256:3bb5229eed6fbbdafc13ce962712ae66e175aa214c69bed35a06bffcf0c5e244"},
]
"backports.zoneinfo" = [
{file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"},
{file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"},
{file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"},
{file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"},
{file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"},
{file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"},
{file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"},
{file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"},
{file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"},
{file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"},
{file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"},
{file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"},
{file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"},
{file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"},
{file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"},
{file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"},
]
black = [
{file = "black-21.9b0-py3-none-any.whl", hash = "sha256:380f1b5da05e5a1429225676655dddb96f5ae8c75bdf91e53d798871b902a115"},
{file = "black-21.9b0.tar.gz", hash = "sha256:7de4cfc7eb6b710de325712d40125689101d21d25283eed7e9998722cf10eb91"},
]
cachetools = [
{file = "cachetools-4.2.2-py3-none-any.whl", hash = "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001"},
{file = "cachetools-4.2.2.tar.gz", hash = "sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff"},
]
certifi = [
{file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"},
{file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"},
]
click = [
{file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"},
{file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"},
]
colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
pathspec = [
{file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
{file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
]
platformdirs = [
{file = "platformdirs-2.3.0-py3-none-any.whl", hash = "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648"},
{file = "platformdirs-2.3.0.tar.gz", hash = "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f"},
]
python-telegram-bot = [
{file = "python-telegram-bot-13.7.tar.gz", hash = "sha256:24df75459e335b96baffa6991679f844bd426978af5a69ca419a0ac43a40602c"},
{file = "python_telegram_bot-13.7-py3-none-any.whl", hash = "sha256:3bf210862744068aa789d5110f8e3a00d98912ce50863384836440a18abf76b5"},
]
pytz = [
{file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"},
{file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"},
]
regex = [
{file = "regex-2021.8.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2"},
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a"},
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0"},
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb"},
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a"},
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308"},
{file = "regex-2021.8.28-cp310-cp310-win32.whl", hash = "sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed"},
{file = "regex-2021.8.28-cp310-cp310-win_amd64.whl", hash = "sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8"},
{file = "regex-2021.8.28-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c"},
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c"},
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13"},
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0"},
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1"},
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f"},
{file = "regex-2021.8.28-cp36-cp36m-win32.whl", hash = "sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354"},
{file = "regex-2021.8.28-cp36-cp36m-win_amd64.whl", hash = "sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645"},
{file = "regex-2021.8.28-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a"},
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e"},
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892"},
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791"},
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759"},
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906"},
{file = "regex-2021.8.28-cp37-cp37m-win32.whl", hash = "sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a"},
{file = "regex-2021.8.28-cp37-cp37m-win_amd64.whl", hash = "sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc"},
{file = "regex-2021.8.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd"},
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797"},
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f"},
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256"},
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b"},
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d5111d4c843d80202e62b4fdbb4920db1dcee4f9366d6b03294f45ed7b18b42e"},
{file = "regex-2021.8.28-cp38-cp38-win32.whl", hash = "sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d"},
{file = "regex-2021.8.28-cp38-cp38-win_amd64.whl", hash = "sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2"},
{file = "regex-2021.8.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468"},
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb"},
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d"},
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983"},
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8"},
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed"},
{file = "regex-2021.8.28-cp39-cp39-win32.whl", hash = "sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374"},
{file = "regex-2021.8.28-cp39-cp39-win_amd64.whl", hash = "sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73"},
{file = "regex-2021.8.28.tar.gz", hash = "sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1"},
]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
tomli = [
{file = "tomli-1.2.1-py3-none-any.whl", hash = "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f"},
{file = "tomli-1.2.1.tar.gz", hash = "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"},
]
tornado = [
{file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"},
{file = "tornado-6.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c"},
{file = "tornado-6.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05"},
{file = "tornado-6.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910"},
{file = "tornado-6.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b"},
{file = "tornado-6.1-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675"},
{file = "tornado-6.1-cp35-cp35m-win32.whl", hash = "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5"},
{file = "tornado-6.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68"},
{file = "tornado-6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb"},
{file = "tornado-6.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c"},
{file = "tornado-6.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921"},
{file = "tornado-6.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558"},
{file = "tornado-6.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c"},
{file = "tornado-6.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085"},
{file = "tornado-6.1-cp36-cp36m-win32.whl", hash = "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575"},
{file = "tornado-6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795"},
{file = "tornado-6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f"},
{file = "tornado-6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102"},
{file = "tornado-6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4"},
{file = "tornado-6.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd"},
{file = "tornado-6.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01"},
{file = "tornado-6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d"},
{file = "tornado-6.1-cp37-cp37m-win32.whl", hash = "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df"},
{file = "tornado-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37"},
{file = "tornado-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95"},
{file = "tornado-6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a"},
{file = "tornado-6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5"},
{file = "tornado-6.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288"},
{file = "tornado-6.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f"},
{file = "tornado-6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6"},
{file = "tornado-6.1-cp38-cp38-win32.whl", hash = "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326"},
{file = "tornado-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c"},
{file = "tornado-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5"},
{file = "tornado-6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe"},
{file = "tornado-6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea"},
{file = "tornado-6.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2"},
{file = "tornado-6.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0"},
{file = "tornado-6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd"},
{file = "tornado-6.1-cp39-cp39-win32.whl", hash = "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c"},
{file = "tornado-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4"},
{file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"},
]
typing-extensions = [
{file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
{file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
{file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
]
tzdata = [
{file = "tzdata-2021.1-py2.py3-none-any.whl", hash = "sha256:9ad21eada54c97001e3e9858a674b3ee6bebe4a4fb2b58465930f2af0ba6c85d"},
{file = "tzdata-2021.1.tar.gz", hash = "sha256:e19c7351f887522a1ac739d21041e592ddde6dd1b764fdefa8f7b2b3551d3d38"},
]
tzlocal = [
{file = "tzlocal-3.0-py3-none-any.whl", hash = "sha256:c736f2540713deb5938d789ca7c3fc25391e9a20803f05b60ec64987cf086559"},
{file = "tzlocal-3.0.tar.gz", hash = "sha256:f4e6e36db50499e0d92f79b67361041f048e2609d166e93456b50746dc4aef12"},
]

16
tgbot/pyproject.toml Normal file
View File

@ -0,0 +1,16 @@
[tool.poetry]
name = "kollagen-bot"
version = "0.1.0"
description = ""
authors = ["Tomáš Mládek <t@mldk.cz>"]
[tool.poetry.dependencies]
python = "^3.8"
python-telegram-bot = "^13.7"
[tool.poetry.dev-dependencies]
black = {version = "^21.9b0", allow-prereleases = true}
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"