;
142 | return <>
143 | {preview}
144 | {print_menu}
145 | >;
146 | }
147 |
--------------------------------------------------------------------------------
/i18nx/i18n.ts:
--------------------------------------------------------------------------------
1 |
2 | ///
3 |
4 | import { CommonExtensionsByLanguage } from "./common-extensions.ts";
5 |
6 | /** Make a plural selector Extension for this language */
7 | function mkpluralext(lang: string) {
8 | const rule = typeof Intl === 'undefined' ? undefined : new Intl.PluralRules(lang);
9 | return function(thing: Thing, v: Variant) {
10 | if (typeof thing !== 'number' || typeof v !== 'object' || !Object.hasOwn(v, 'one'))
11 | return undefined;
12 | if (rule === undefined)
13 | return thing === 1 ? 'one' : 'other';
14 | else
15 | return rule.select(thing);
16 | }
17 | }
18 | /** Make a number formatter Extension for this language */
19 | function mknumberfmtext(lang: string) {
20 | if (typeof Intl === 'undefined')
21 | return () => undefined;
22 | const formatter = new Intl.NumberFormat(lang);
23 | return function(thing: Thing, _v: Variant) {
24 | if (typeof thing !== 'number') return undefined;
25 | return { thing: formatter.format(thing) };
26 | }
27 | }
28 |
29 | /**
30 | * I18nx: I18n, Extensible
31 | */
32 | export default class I18nx {
33 | /** I18n data so far by language */
34 | public data: Record;
35 | /** Language priority list */
36 | public stack: string[];
37 | /** Existing Extensions by language */
38 | public extensions: Record;
39 | /**
40 | * Initialize i18nx class
41 | * @param use_preset_exts There are some extensions built in: plural rules, number representation, etc. Use `true` to enable them, which is the default.
42 | */
43 | constructor(public use_preset_exts: boolean = true) {
44 | this.data = {};
45 | this.stack = [];
46 | this.extensions = {};
47 | }
48 | /**
49 | * Don’t use this language (primarily or as fallback) anymore. Existing i18n data is untouched.
50 | * @param lang Language code
51 | */
52 | remove(lang: string) {
53 | for (let i = 0; i < this.stack.length; i++)
54 | if (this.stack[i] === lang)
55 | this.stack.splice(i, 1);
56 | }
57 | /**
58 | * Initialize this language. Do this before updating/extending/using it.
59 | * @param lang Language code
60 | */
61 | add(lang: string) {
62 | this.data[lang] = {};
63 | const exts: Extension[] = this.extensions[lang] = [];
64 | const common_exts = CommonExtensionsByLanguage[lang];
65 | if (this.use_preset_exts) {
66 | exts.push(mkpluralext(lang));
67 | exts.push(mknumberfmtext(lang));
68 | if (common_exts)
69 | exts.push(...common_exts);
70 | }
71 | }
72 | /**
73 | * Put this language as a fallback. Useful for initializing languages from like `navigator.languages`
74 | * @param lang Language code
75 | */
76 | append(lang: string) {
77 | this.remove(lang);
78 | this.stack.push(lang);
79 | }
80 | /**
81 | * Put this language as the first to use. Useful for setting user language on demand.
82 | * @param lang Language code
83 | */
84 | prepend(lang: string) {
85 | this.remove(lang);
86 | this.stack.unshift(lang);
87 | }
88 | /**
89 | * Reset language preference stack.
90 | */
91 | reset() {
92 | this.stack.splice(0, this.stack.length);
93 | }
94 | /**
95 | * Add/Update i18n data for this language. Existing strings are overwritten.
96 | * @param lang Language code
97 | * @param data JavaScript Object for i18n data
98 | */
99 | update(lang: string, data: I18nData) {
100 | if (!lang) lang = this.stack[0];
101 | for (const key in data)
102 | this.data[lang][key] = data[key];
103 | }
104 | /**
105 | * Add a function for this language as an Extension. With highest priority.
106 | * @param lang Language code
107 | * @param ext An Extension
108 | */
109 | extend(lang: string, ext: Extension) {
110 | if (!lang) lang = this.stack[0];
111 | this.extensions[lang].unshift(ext);
112 | }
113 | /**
114 | * Translate a string to certain language by priority with existing data.
115 | * @param string string to be translated
116 | * @param things arguments, number or string, singular or many in a list
117 | * @returns translated string
118 | */
119 | translate(string: string, things?: Thing | Things): string {
120 | let language: string | undefined;
121 | let variants: Variants | undefined;
122 | for (const lang of this.stack)
123 | if (this.data[lang] !== undefined && this.data[lang][string] !== undefined) {
124 | language = lang;
125 | variants = this.data[lang][string];
126 | break;
127 | }
128 | if (language === undefined || variants === undefined)
129 | return string;
130 | if (typeof variants === 'string' && things === undefined)
131 | return variants;
132 | if (typeof things === 'undefined')
133 | things = [];
134 | else if (typeof things === 'number' || typeof things === 'string')
135 | things = [things];
136 | const holdplace = (s: string, i: number, p: string) => s.replace(i === -1 ? '{}' : `{${i}}`, p);
137 | if (typeof variants === 'string')
138 | return things.map((thing, i) => variants = holdplace(variants as string, i, thing.toString())), variants;
139 | const apply_variants: (v: Variant, t: Thing, i: number) => string
140 | = (v: Variant, t: Thing, i: number) => {
141 | if (typeof v === 'string')
142 | return holdplace(v, i, t.toString());
143 | else {
144 | let extresult: ExtensionReturns;
145 | let k: string | undefined;
146 | let m: Thing | undefined;
147 | for (const ext of this.extensions[language!])
148 | if ((extresult = ext(m || t, v))) {
149 | if (typeof extresult === 'object') {
150 | m = extresult.thing || m;
151 | k = extresult.key || k;
152 | }
153 | else
154 | k = extresult;
155 | if (k !== undefined && m !== undefined)
156 | break;
157 | }
158 | if (m === undefined) m = t;
159 | if (k === undefined) return m.toString();
160 | if (typeof v[k] === 'undefined')
161 | return m.toString();
162 | else if (typeof v[k] === 'string')
163 | return holdplace(v[k] as string, -1, m.toString());
164 | else
165 | return holdplace(v[k][0] as string, -1, apply_variants(v[k][1], m, -1));
166 | }
167 | }
168 | let result = variants[0] as string;
169 | for (let i = 0; i < things.length; i++)
170 | result = holdplace(result, i, apply_variants(variants[i + 1], things[i], i));
171 | return result;
172 | }
173 | /** Shortcut for I18nx.translate */
174 | _ = this.translate.bind(this);
175 | }
176 |
--------------------------------------------------------------------------------
/CC0-1.0.txt:
--------------------------------------------------------------------------------
1 | Creative Commons Legal Code
2 |
3 | CC0 1.0 Universal
4 |
5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
12 | HEREUNDER.
13 |
14 | Statement of Purpose
15 |
16 | The laws of most jurisdictions throughout the world automatically confer
17 | exclusive Copyright and Related Rights (defined below) upon the creator
18 | and subsequent owner(s) (each and all, an "owner") of an original work of
19 | authorship and/or a database (each, a "Work").
20 |
21 | Certain owners wish to permanently relinquish those rights to a Work for
22 | the purpose of contributing to a commons of creative, cultural and
23 | scientific works ("Commons") that the public can reliably and without fear
24 | of later claims of infringement build upon, modify, incorporate in other
25 | works, reuse and redistribute as freely as possible in any form whatsoever
26 | and for any purposes, including without limitation commercial purposes.
27 | These owners may contribute to the Commons to promote the ideal of a free
28 | culture and the further production of creative, cultural and scientific
29 | works, or to gain reputation or greater distribution for their Work in
30 | part through the use and efforts of others.
31 |
32 | For these and/or other purposes and motivations, and without any
33 | expectation of additional consideration or compensation, the person
34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she
35 | is an owner of Copyright and Related Rights in the Work, voluntarily
36 | elects to apply CC0 to the Work and publicly distribute the Work under its
37 | terms, with knowledge of his or her Copyright and Related Rights in the
38 | Work and the meaning and intended legal effect of CC0 on those rights.
39 |
40 | 1. Copyright and Related Rights. A Work made available under CC0 may be
41 | protected by copyright and related or neighboring rights ("Copyright and
42 | Related Rights"). Copyright and Related Rights include, but are not
43 | limited to, the following:
44 |
45 | i. the right to reproduce, adapt, distribute, perform, display,
46 | communicate, and translate a Work;
47 | ii. moral rights retained by the original author(s) and/or performer(s);
48 | iii. publicity and privacy rights pertaining to a person's image or
49 | likeness depicted in a Work;
50 | iv. rights protecting against unfair competition in regards to a Work,
51 | subject to the limitations in paragraph 4(a), below;
52 | v. rights protecting the extraction, dissemination, use and reuse of data
53 | in a Work;
54 | vi. database rights (such as those arising under Directive 96/9/EC of the
55 | European Parliament and of the Council of 11 March 1996 on the legal
56 | protection of databases, and under any national implementation
57 | thereof, including any amended or successor version of such
58 | directive); and
59 | vii. other similar, equivalent or corresponding rights throughout the
60 | world based on applicable law or treaty, and any national
61 | implementations thereof.
62 |
63 | 2. Waiver. To the greatest extent permitted by, but not in contravention
64 | of, applicable law, Affirmer hereby overtly, fully, permanently,
65 | irrevocably and unconditionally waives, abandons, and surrenders all of
66 | Affirmer's Copyright and Related Rights and associated claims and causes
67 | of action, whether now known or unknown (including existing as well as
68 | future claims and causes of action), in the Work (i) in all territories
69 | worldwide, (ii) for the maximum duration provided by applicable law or
70 | treaty (including future time extensions), (iii) in any current or future
71 | medium and for any number of copies, and (iv) for any purpose whatsoever,
72 | including without limitation commercial, advertising or promotional
73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
74 | member of the public at large and to the detriment of Affirmer's heirs and
75 | successors, fully intending that such Waiver shall not be subject to
76 | revocation, rescission, cancellation, termination, or any other legal or
77 | equitable action to disrupt the quiet enjoyment of the Work by the public
78 | as contemplated by Affirmer's express Statement of Purpose.
79 |
80 | 3. Public License Fallback. Should any part of the Waiver for any reason
81 | be judged legally invalid or ineffective under applicable law, then the
82 | Waiver shall be preserved to the maximum extent permitted taking into
83 | account Affirmer's express Statement of Purpose. In addition, to the
84 | extent the Waiver is so judged Affirmer hereby grants to each affected
85 | person a royalty-free, non transferable, non sublicensable, non exclusive,
86 | irrevocable and unconditional license to exercise Affirmer's Copyright and
87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the
88 | maximum duration provided by applicable law or treaty (including future
89 | time extensions), (iii) in any current or future medium and for any number
90 | of copies, and (iv) for any purpose whatsoever, including without
91 | limitation commercial, advertising or promotional purposes (the
92 | "License"). The License shall be deemed effective as of the date CC0 was
93 | applied by Affirmer to the Work. Should any part of the License for any
94 | reason be judged legally invalid or ineffective under applicable law, such
95 | partial invalidity or ineffectiveness shall not invalidate the remainder
96 | of the License, and in such case Affirmer hereby affirms that he or she
97 | will not (i) exercise any of his or her remaining Copyright and Related
98 | Rights in the Work or (ii) assert any associated claims and causes of
99 | action with respect to the Work, in either case contrary to Affirmer's
100 | express Statement of Purpose.
101 |
102 | 4. Limitations and Disclaimers.
103 |
104 | a. No trademark or patent rights held by Affirmer are waived, abandoned,
105 | surrendered, licensed or otherwise affected by this document.
106 | b. Affirmer offers the Work as-is and makes no representations or
107 | warranties of any kind concerning the Work, express, implied,
108 | statutory or otherwise, including without limitation warranties of
109 | title, merchantability, fitness for a particular purpose, non
110 | infringement, or the absence of latent or other defects, accuracy, or
111 | the present or absence of errors, whether or not discoverable, all to
112 | the greatest extent permissible under applicable law.
113 | c. Affirmer disclaims responsibility for clearing rights of other persons
114 | that may apply to the Work or any use thereof, including without
115 | limitation any person's Copyright and Related Rights in the Work.
116 | Further, Affirmer disclaims responsibility for obtaining any necessary
117 | consents, permissions or other rights required for any use of the
118 | Work.
119 | d. Affirmer understands and acknowledges that Creative Commons is not a
120 | party to this document and has no duty or obligation with respect to
121 | this CC0 or use of the Work.
122 |
--------------------------------------------------------------------------------
/components/StuffPreview.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "preact/hooks";
2 | import { ImageWorkerMessage, StuffPainterProps } from "../common/types.ts";
3 | import { mkcanvas } from "../common/utility.ts";
4 | //@ts-ignore: outdated type annotation from cdn
5 | import { splitText } from 'canvas-txt'
6 | import { IS_BROWSER } from "$fresh/runtime.ts";
7 | import { STUFF_PAINT_INIT_URL } from "../common/constants.ts";
8 | import { _ } from "../common/i18n.tsx";
9 |
10 | declare function splitText(args: {
11 | ctx: CanvasRenderingContext2D,
12 | text: string,
13 | justify?: boolean,
14 | width?: number
15 | }): string[];
16 |
17 | const image_worker = IS_BROWSER ? new Worker('/image_worker.js') : null;
18 |
19 | export default function StuffPreview(props: StuffPainterProps) {
20 | const [imgsrc, set_imgsrc] = useState(STUFF_PAINT_INIT_URL);
21 | if (!IS_BROWSER) return ;
22 | const { canvas, width, ctx, img } = mkcanvas(props.width);
23 | const stuff = props.stuff;
24 | let font;
25 | let strings: string[];
26 | let measured: TextMetrics[];
27 | let line_height;
28 | let msg: ImageWorkerMessage;
29 | let imagedata: ImageData;
30 | (async function() {
31 | switch (stuff.type) {
32 | case 'text':
33 | ctx.fillStyle = 'black';
34 | ctx.strokeStyle = 'black';
35 | font = `${stuff.textFontWeight} ${stuff.textFontSize}px "${stuff.textFontFamily}"`;
36 | // ctx.font is set multiple times intensionally
37 | ctx.font = font;
38 | const is_rotated_sideways = stuff.rotate === 90 || stuff.rotate === 270;
39 | strings = splitText({
40 | ctx: ctx,
41 | text: stuff.textContent!,
42 | justify: stuff.textAlign === 'justify',
43 | width: is_rotated_sideways ? 10000 : width
44 | });
45 | ctx.font = font;
46 | measured = strings.map(s => ctx.measureText(s));
47 | line_height = stuff.textLineSpacing! + Math.max(...measured.map(m => m.actualBoundingBoxAscent), stuff.textFontSize!);
48 |
49 | const text_intrinsic_height = line_height * strings.length + stuff.textLineSpacing!;
50 |
51 | const needs_flip = is_rotated_sideways
52 | ? (stuff.rotate === 90) !== !!stuff.flipV
53 | : (stuff.rotate === 180) !== !!stuff.flipH;
54 |
55 | let effectiveTextAlign = stuff.textAlign;
56 | if (needs_flip) {
57 | if (stuff.textAlign === 'start') effectiveTextAlign = 'end';
58 | else if (stuff.textAlign === 'end') effectiveTextAlign = 'start';
59 | }
60 | const shiftMultiplier = needs_flip ? -1 : 1;
61 |
62 | let y_offset = 0;
63 |
64 | if (is_rotated_sideways) {
65 | const text_intrinsic_width = Math.max(...measured.map(m => m.width));
66 | canvas.width = text_intrinsic_width > 0 ? text_intrinsic_width : 1;
67 | canvas.height = width; // Use preview width for the height, which will be rotated to become the new width.
68 |
69 | let h_align_offset = 0;
70 | switch (effectiveTextAlign) {
71 | case 'center':
72 | h_align_offset = (canvas.height - text_intrinsic_height) / 2;
73 | break;
74 | case 'end':
75 | h_align_offset = canvas.height - text_intrinsic_height;
76 | break;
77 | }
78 | const h_shift_offset = shiftMultiplier * stuff.textShift! * canvas.height;
79 | y_offset = h_align_offset + h_shift_offset;
80 | } else {
81 | canvas.height = text_intrinsic_height > 0 ? text_intrinsic_height : 1;
82 | }
83 |
84 | ctx.font = font;
85 | ctx.fillStyle = 'black';
86 | ctx.strokeStyle = 'black';
87 | ctx.textAlign = 'start';
88 | for (let i = 0; i < strings.length; ++i) {
89 | const s = strings[i];
90 | let anchor_x;
91 |
92 | if (is_rotated_sideways) {
93 | const text_block_width = canvas.width;
94 | anchor_x = ({
95 | 'start': 0,
96 | 'center': (text_block_width - measured[i].width) / 2,
97 | 'end': text_block_width - measured[i].width,
98 | 'justify': 0
99 | })[stuff.textAlign!];
100 | } else {
101 | anchor_x = ({
102 | 'start': 0,
103 | 'center': (width - measured[i].width) / 2,
104 | 'end': width - measured[i].width,
105 | 'justify': 0
106 | })[effectiveTextAlign] + (shiftMultiplier * stuff.textShift! * width);
107 | }
108 |
109 | const anchor_y = y_offset + line_height * (i + 1);
110 |
111 | if (stuff.textStroked) {
112 | ctx.strokeText(s, anchor_x, anchor_y);
113 | } else {
114 | ctx.fillText(s, anchor_x, anchor_y);
115 | }
116 | }
117 | break;
118 | case 'pic':
119 | img.src = stuff.picUrl!;
120 | await new Promise(resolve =>
121 | img.addEventListener('load', () => resolve(), { once: true }));
122 | if (stuff.rotate === 0 || stuff.rotate === 180) {
123 | img.height = width / (img.width / img.height) | 0;
124 | img.width = width;
125 | } else {
126 | img.width = width * (img.width / img.height) | 0;
127 | img.height = width;
128 | }
129 | canvas.width = img.width;
130 | canvas.height = img.height;
131 | ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
132 | break;
133 | }
134 | image_worker!.postMessage({
135 | id: stuff.id,
136 | dither: stuff.dither,
137 | rotate: stuff.rotate,
138 | flip: stuff.flipH ? (stuff.flipV ? 'both' : 'h') : (stuff.flipV ? 'v' : 'none'),
139 | brightness: stuff.brightness,
140 | data: ctx.getImageData(0, 0, canvas.width, canvas.height).data.buffer,
141 | width: canvas.width,
142 | height: canvas.height
143 | })
144 | msg = await new Promise(resolve => {
145 | const callback = (event: MessageEvent) => {
146 | if (event.data.id !== stuff.id) return;
147 | // { once: true } doesn't work on this one
148 | image_worker!.removeEventListener('message', callback);
149 | resolve(event.data);
150 | };
151 | image_worker!.addEventListener('message', callback);
152 | });
153 | stuff.width = canvas.width = msg.width;
154 | stuff.height = canvas.height = msg.height;
155 | imagedata = new ImageData(new Uint8ClampedArray(msg.data), msg.width, msg.height);
156 | ctx.putImageData(imagedata, 0, 0);
157 | if (canvas.height !== 0) {
158 | const url = canvas.toDataURL();
159 | set_imgsrc(url);
160 | props.dispatch({
161 | index: props.index,
162 | width: canvas.width,
163 | height: canvas.height,
164 | data: new Uint8Array(imagedata.data.buffer)
165 | });
166 | }
167 | })();
168 | return