diff --git a/examples/package.json b/examples/package.json index 963ac39d25f..e11373fece8 100644 --- a/examples/package.json +++ b/examples/package.json @@ -67,6 +67,7 @@ "@univerjs/uni-slides-ui": "workspace:*", "@univerjs/uniscript": "workspace:*", "@univerjs/uniui": "workspace:*", + "@univerjs/watermark": "workspace:*", "clsx": "^2.1.1", "monaco-editor": "0.52.0", "react": "18.3.1", diff --git a/examples/src/sheets/very-lazy.ts b/examples/src/sheets/very-lazy.ts index 520185190c3..950e6f7ab45 100644 --- a/examples/src/sheets/very-lazy.ts +++ b/examples/src/sheets/very-lazy.ts @@ -22,6 +22,7 @@ import { UniverSheetsFindReplacePlugin } from '@univerjs/sheets-find-replace'; import { UniverSheetsHyperLinkUIPlugin } from '@univerjs/sheets-hyper-link-ui'; import { UniverSheetsSortUIPlugin } from '@univerjs/sheets-sort-ui'; import { UniverUniscriptPlugin } from '@univerjs/uniscript'; +import { UniverWatermarkPlugin } from '@univerjs/watermark'; /* eslint-disable-next-line node/prefer-global/process */ const IS_E2E: boolean = !!process.env.IS_E2E; @@ -50,6 +51,7 @@ export default function getVeryLazyPlugins() { [UniverSheetsSortUIPlugin], [UniverSheetsCrosshairHighlightPlugin], [UniverSheetsFindReplacePlugin], + [UniverWatermarkPlugin], ]; if (!IS_E2E) { diff --git a/packages/docs-ui/src/services/docs-render.service.ts b/packages/docs-ui/src/services/docs-render.service.ts index 90712b3ef02..26b1a29ae57 100644 --- a/packages/docs-ui/src/services/docs-render.service.ts +++ b/packages/docs-ui/src/services/docs-render.service.ts @@ -14,11 +14,13 @@ * limitations under the License. */ -import type { DocumentDataModel } from '@univerjs/core'; +import type { DocumentDataModel, Workbook } from '@univerjs/core'; import { IUniverInstanceService, RxDisposable, UniverInstanceType } from '@univerjs/core'; import { IRenderManagerService } from '@univerjs/engine-render'; import { takeUntil } from 'rxjs'; +const DOC_MAIN_CANVAS_ID = 'univer-doc-main-canvas'; + export class DocsRenderService extends RxDisposable { constructor( @IUniverInstanceService private readonly _instanceSrv: IUniverInstanceService, @@ -48,7 +50,13 @@ export class DocsRenderService extends RxDisposable { private _createRenderer(doc: DocumentDataModel) { const unitId = doc.getUnitId(); - + const workbookId = this._instanceSrv.getCurrentUnitForType(UniverInstanceType.UNIVER_DOC)?.getUnitId(); + this._renderManagerService.created$.subscribe((renderer) => { + if (renderer.unitId === workbookId) { + renderer.engine.getCanvas().setId(DOC_MAIN_CANVAS_ID); + renderer.engine.getCanvas().getContext().setId(DOC_MAIN_CANVAS_ID); + } + }); if (!this._renderManagerService.has(unitId)) { this._createRenderWithId(unitId); diff --git a/packages/engine-render/src/context.ts b/packages/engine-render/src/context.ts index 31d4613bb8d..37647289111 100644 --- a/packages/engine-render/src/context.ts +++ b/packages/engine-render/src/context.ts @@ -34,6 +34,16 @@ export class UniverRenderingContext2D implements CanvasRenderingContext2D { this._context = context; } + private _id: string; + + getId() { + return this._id; + } + + setId(id: string) { + this._id = id; + } + isContextLost(): boolean { // @ts-ignore return this._context.isContextLost(); @@ -438,10 +448,10 @@ export class UniverRenderingContext2D implements CanvasRenderingContext2D { this._context.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y); } - /** - * bezierCurveTo function precision. - * @method - */ + /** + * bezierCurveTo function precision. + * @method + */ bezierCurveToByPrecision(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number) { const { scaleX, scaleY } = this._getScale(); x = fixLineWidthByScale(x, scaleX); @@ -673,10 +683,10 @@ export class UniverRenderingContext2D implements CanvasRenderingContext2D { this._context.fillRect(x, y, width, height); } - /** - * fillRect function precision. - * @method - */ + /** + * fillRect function precision. + * @method + */ fillRectByPrecision(x: number, y: number, width: number, height: number) { const { scaleX, scaleY } = this._getScale(); x = fixLineWidthByScale(x, scaleX); diff --git a/packages/facade/package.json b/packages/facade/package.json index af7744b2682..927083a6d39 100644 --- a/packages/facade/package.json +++ b/packages/facade/package.json @@ -84,7 +84,8 @@ "@univerjs/sheets-ui": "workspace:*", "@univerjs/thread-comment": "workspace:*", "@univerjs/thread-comment-ui": "workspace:*", - "@univerjs/ui": "workspace:*" + "@univerjs/ui": "workspace:*", + "@univerjs/watermark": "workspace:*" }, "devDependencies": { "@univerjs-infra/shared": "workspace:*", diff --git a/packages/facade/src/apis/facade.ts b/packages/facade/src/apis/facade.ts index b17e2d5ee81..de5360d4204 100644 --- a/packages/facade/src/apis/facade.ts +++ b/packages/facade/src/apis/facade.ts @@ -37,6 +37,8 @@ import type { import type { ISocket } from '@univerjs/network'; import type { ISetCrosshairHighlightColorOperationParams } from '@univerjs/sheets-crosshair-highlight'; import type { IRegisterFunctionParams } from '@univerjs/sheets-formula'; +import type { IImageWatermarkConfig, ITextWatermarkConfig } from '@univerjs/watermark'; + import { BorderStyleTypes, debounce, @@ -53,7 +55,6 @@ import { UniverInstanceType, WrapStrategy, } from '@univerjs/core'; - import { SetFormulaCalculationStartMutation } from '@univerjs/engine-formula'; import { IRenderManagerService } from '@univerjs/engine-render'; import { ISocketService, WebSocketService } from '@univerjs/network'; @@ -61,6 +62,7 @@ import { DisableCrosshairHighlightOperation, EnableCrosshairHighlightOperation, import { IRegisterFunctionService, RegisterFunctionService } from '@univerjs/sheets-formula'; import { SHEET_VIEW_KEY } from '@univerjs/sheets-ui'; import { CopyCommand, PasteCommand } from '@univerjs/ui'; +import { IWatermarkTypeEnum, WatermarkImageBaseConfig, WatermarkService, WatermarkTextBaseConfig } from '@univerjs/watermark'; import { FDocument } from './docs/f-document'; import { FHooks } from './f-hooks'; import { FDataValidationBuilder } from './sheets/f-data-validation-builder'; @@ -536,4 +538,59 @@ export class FUniver { getPermission(): FPermission { return this._injector.createInstance(FPermission); } + + // #region watermark + + /** + * Adds a watermark to the unit. Supports both text and image watermarks based on the specified type. + * + * @param {IWatermarkTypeEnum.Text | IWatermarkTypeEnum.Image} type - The type of watermark to add. Can be either 'Text' or 'Image'. + * @param {ITextWatermarkConfig | IImageWatermarkConfig} config - The configuration object for the watermark. + * - If the type is 'Text', the config should follow the ITextWatermarkConfig interface. + * - If the type is 'Image', the config should follow the IImageWatermarkConfig interface. + * @throws {Error} Throws an error if the watermark type is unknown. + */ + addWatermark(type: IWatermarkTypeEnum.Text, config: ITextWatermarkConfig): void; + addWatermark(type: IWatermarkTypeEnum.Image, config: IImageWatermarkConfig): void; + addWatermark( + type: IWatermarkTypeEnum.Text | IWatermarkTypeEnum.Image, + config: ITextWatermarkConfig | IImageWatermarkConfig + ): void { + const watermarkService = this._injector.get(WatermarkService); + if (type === IWatermarkTypeEnum.Text) { + watermarkService.updateWatermarkConfig({ + type: IWatermarkTypeEnum.Text, + config: { + text: { + ...WatermarkTextBaseConfig, + ...config, + }, + }, + }); + } else if (type === IWatermarkTypeEnum.Image) { + watermarkService.updateWatermarkConfig({ + type: IWatermarkTypeEnum.Image, + config: { + image: { + ...WatermarkImageBaseConfig, + ...config, + }, + }, + }); + } else { + throw new Error('Unknown watermark type'); + } + } + + /** + * Deletes the currently applied watermark from the unit. + * + * This function retrieves the watermark service and invokes the method to remove any existing watermark configuration. + */ + deleteWatermark(): void { + const watermarkService = this._injector.get(WatermarkService); + watermarkService.deleteWatermarkConfig(); + } + + // #endregion } diff --git a/packages/facade/src/apis/sheets/f-workbook.ts b/packages/facade/src/apis/sheets/f-workbook.ts index f924e9133ab..e4d2ff16574 100644 --- a/packages/facade/src/apis/sheets/f-workbook.ts +++ b/packages/facade/src/apis/sheets/f-workbook.ts @@ -26,6 +26,7 @@ import type { ICanvasFloatDom } from '@univerjs/sheets-drawing-ui'; import type { ISheetHyperLinkInfo } from '@univerjs/sheets-hyper-link-ui'; import type { CommentUpdate, IAddCommentCommandParams, IDeleteCommentCommandParams } from '@univerjs/thread-comment'; import type { IDialogPartMethodOptions, ISidebarMethodOptions } from '@univerjs/ui'; + import type { IFComponentKey } from './utils'; import { ICommandService, @@ -46,6 +47,7 @@ import { AddSheetDataValidationCommand, RemoveSheetAllDataValidationCommand, Rem import { SheetsHyperLinkResolverService } from '@univerjs/sheets-hyper-link-ui'; import { AddCommentCommand, DeleteCommentCommand, DeleteCommentTreeCommand, ThreadCommentModel, UpdateCommentCommand } from '@univerjs/thread-comment'; import { IDialogService, ISidebarService } from '@univerjs/ui'; + import { filter } from 'rxjs'; import { FRange } from './f-range'; import { FWorksheet } from './f-worksheet'; diff --git a/packages/sheets-ui/src/services/sheets-render.service.ts b/packages/sheets-ui/src/services/sheets-render.service.ts index 7ce36168b1b..85c6eb35d0f 100644 --- a/packages/sheets-ui/src/services/sheets-render.service.ts +++ b/packages/sheets-ui/src/services/sheets-render.service.ts @@ -51,7 +51,7 @@ export class SheetsRenderService extends RxDisposable { */ registerSkeletonChangingMutations(mutationId: string): IDisposable { if (this._skeletonChangeMutations.has(mutationId)) { - return toDisposable(() => {}); + return toDisposable(() => { }); } this._skeletonChangeMutations.add(mutationId); @@ -87,6 +87,7 @@ export class SheetsRenderService extends RxDisposable { this._renderManagerService.created$.subscribe((renderer) => { if (renderer.unitId === unitId) { renderer.engine.getCanvas().setId(`${SHEET_MAIN_CANVAS_ID}_${unitId}`); + renderer.engine.getCanvas().getContext().setId(`${SHEET_MAIN_CANVAS_ID}_${unitId}`); } }); this._renderManagerService.createRender(unitId); diff --git a/packages/watermark/README.md b/packages/watermark/README.md new file mode 100644 index 00000000000..cb08aba2189 --- /dev/null +++ b/packages/watermark/README.md @@ -0,0 +1,37 @@ +# @univerjs/watermark + +## Package Overview + +| Package Name | UMD Namespace | Version | License | Downloads | Contains CSS | Contains i18n locales | +| --- | --- | --- | --- | --- | :---: | :---: | +| `@univerjs/watermark` | `UniverWatermark` | [![][npm-version-shield]][npm-version-link] | ![][npm-license-shield] | ![][npm-downloads-shield] | ⭕️ | ⭕️ | + +## Introduction + +`@univerjs/watermark` Provides Univer with the ability to add watermarks + +## Usage + +### Installation + +```shell +# Using npm +npm install @univerjs/watermark + +# Using pnpm +pnpm add @univerjs/watermark +``` + +### Register the plugin + +```typescript +import { UniverWatermarkPlugin } from '@univerjs/watermark'; + +univer.registerPlugin(UniverWatermarkPlugin); +``` + + +[npm-version-shield]: https://img.shields.io/npm/v/@univerjs/watermark?style=flat-square +[npm-version-link]: https://npmjs.com/package/@univerjs/watermark +[npm-license-shield]: https://img.shields.io/npm/l/@univerjs/watermark?style=flat-square +[npm-downloads-shield]: https://img.shields.io/npm/dm/@univerjs/watermark?style=flat-square diff --git a/packages/watermark/package.json b/packages/watermark/package.json new file mode 100644 index 00000000000..f4eac4f48ac --- /dev/null +++ b/packages/watermark/package.json @@ -0,0 +1,104 @@ +{ + "name": "@univerjs/watermark", + "version": "0.3.0", + "private": false, + "description": "univer watermark plugin", + "author": "DreamNum ", + "license": "Apache-2.0", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/univer" + }, + "homepage": "https://univer.ai", + "repository": { + "type": "git", + "url": "https://github.com/dream-num/univer" + }, + "bugs": { + "url": "https://github.com/dream-num/univer/issues" + }, + "keywords": [ + "univer" + ], + "exports": { + ".": "./src/index.ts", + "./*": "./src/*", + "./locale/*": "./src/locale/*.ts" + }, + "main": "./lib/cjs/index.js", + "module": "./lib/es/index.js", + "types": "./lib/types/index.d.ts", + "publishConfig": { + "access": "public", + "main": "./lib/cjs/index.js", + "module": "./lib/es/index.js", + "exports": { + ".": { + "import": "./lib/es/index.js", + "require": "./lib/cjs/index.js", + "types": "./lib/types/index.d.ts" + }, + "./*": { + "import": "./lib/es/*", + "require": "./lib/cjs/*", + "types": "./lib/types/index.d.ts" + }, + "./lib/*": "./lib/*", + "./locale/*": "./lib/locale/*.json" + } + }, + "directories": { + "lib": "lib" + }, + "files": [ + "lib" + ], + "scripts": { + "test": "vitest run", + "test:watch": "vitest", + "coverage": "vitest run --coverage", + "lint:types": "tsc --noEmit", + "build": "tsc && vite build" + }, + "peerDependencies": { + "@univerjs/core": "workspace:*", + "@univerjs/design": "workspace:*", + "@univerjs/engine-render": "workspace:*", + "@univerjs/ui": "workspace:*", + "clsx": ">=2.0.0", + "react": "^16.9.0 || ^17.0.0 || ^18.0.0", + "rxjs": ">=7.0.0" + }, + "dependencies": { + "@univerjs/core": "workspace:*", + "@univerjs/design": "workspace:*", + "@univerjs/engine-render": "workspace:*", + "@univerjs/icons": "^0.1.80", + "@univerjs/ui": "workspace:*", + "clsx": "^2.1.1" + }, + "devDependencies": { + "@types/react": "^18.3.9", + "@univerjs-infra/shared": "workspace:*", + "less": "^4.2.0", + "react": "18.3.1", + "rxjs": "^7.8.1", + "typescript": "^5.6.2", + "vite": "^5.4.8", + "vitest": "^2.1.1" + }, + "univerSpace": { + ".": { + "import": "./lib/es/index.js", + "require": "./lib/cjs/index.js", + "types": "./lib/types/index.d.ts" + }, + "./*": { + "import": "./lib/es/*", + "require": "./lib/cjs/*", + "types": "./lib/types/index.d.ts" + }, + "./lib/*": "./lib/*", + "./locale/*": "./lib/locale/*.json" + } +} diff --git a/packages/watermark/src/commands/operations/open-watermark-panel.operation.ts b/packages/watermark/src/commands/operations/open-watermark-panel.operation.ts new file mode 100644 index 00000000000..33236253e76 --- /dev/null +++ b/packages/watermark/src/commands/operations/open-watermark-panel.operation.ts @@ -0,0 +1,39 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ICommand } from '@univerjs/core'; +import { CommandType, LocaleService } from '@univerjs/core'; +import { ISidebarService } from '@univerjs/ui'; +import { WATERMARK_PANEL, WATERMARK_PANEL_FOOTER } from '../../common/const'; + +export const OpenWatermarkPanelOperation: ICommand = { + type: CommandType.OPERATION, + id: 'univer.operation.open-watermark-panel', + handler(accessor) { + const sidebarService = accessor.get(ISidebarService); + const localeService = accessor.get(LocaleService); + + sidebarService.open({ + header: { title: localeService.t('univer-watermark.title') }, + children: { label: WATERMARK_PANEL }, + footer: { label: WATERMARK_PANEL_FOOTER }, + onClose: () => { }, + width: 330, + }); + + return true; + }, +}; diff --git a/packages/watermark/src/common/const.ts b/packages/watermark/src/common/const.ts new file mode 100644 index 00000000000..54a76d7c863 --- /dev/null +++ b/packages/watermark/src/common/const.ts @@ -0,0 +1,79 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { IImageWatermarkConfig, ITextWatermarkConfig, IUserInfoWatermarkConfig } from './type'; + +export const UNIVER_WATERMARK_MENU = 'UNIVER_WATERMARK_MENU'; + +export const WATERMARK_PANEL = 'WATERMARK_PANEL'; + +export const WATERMARK_PANEL_FOOTER = 'WATERMARK_PANEL_FOOTER'; + +export const UNIVER_WATERMARK_STORAGE_KEY = 'UNIVER_WATERMARK_STORAGE_KEY'; + +export const UNIVER_WATERMARK_LAYER_INDEX = 10; + +export const WATERMARK_IMAGE_ALLOW_IMAGE_LIST = ['image/png', 'image/jpeg', 'image/jpg', 'image/bmp']; + +export const WatermarkTextBaseConfig: ITextWatermarkConfig = { + content: '', + fontSize: 16, + color: 'rgb(0,0,0)', + bold: false, + italic: false, + direction: 'ltr', + x: 60, + y: 36, + repeat: true, + spacingX: 200, + spacingY: 100, + rotate: 0, + opacity: 0.15, +}; + +export const WatermarkImageBaseConfig: IImageWatermarkConfig = { + url: '', + width: 100, + height: 100, + maintainAspectRatio: true, + originRatio: 1, + x: 60, + y: 36, + repeat: true, + spacingX: 200, + spacingY: 100, + rotate: 0, + opacity: 0.15, +}; + +export const WatermarkUserInfoBaseConfig: IUserInfoWatermarkConfig = { + name: true, + email: false, + phone: false, + uid: false, + fontSize: 16, + color: 'rgb(0,0,0)', + bold: false, + italic: false, + direction: 'ltr', + x: 60, + y: 60, + repeat: true, + spacingX: 200, + spacingY: 100, + rotate: -30, + opacity: 0.15, +}; diff --git a/packages/watermark/src/common/type.ts b/packages/watermark/src/common/type.ts new file mode 100644 index 00000000000..94189e72192 --- /dev/null +++ b/packages/watermark/src/common/type.ts @@ -0,0 +1,71 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export enum IWatermarkTypeEnum { + // UserInfo is provided for use in the plugin registration place and has the highest priority + UserInfo = 'userInfo', + Text = 'text', + Image = 'image', +} + +export interface IGeneralWatermarkConfig { + // origin position + x: number; + y: number; + repeat: boolean; + // spacing between watermarks + spacingX: number; + spacingY: number; + rotate: number; + opacity: number; + +} + +export interface ITextWatermarkConfig extends IGeneralWatermarkConfig { + content?: string; + fontSize: number; + color: string; + bold: boolean; + italic: boolean; + direction: 'ltr' | 'rtl' | 'inherit'; +} + +export interface IImageWatermarkConfig extends IGeneralWatermarkConfig { + url: string; + width: number; + height: number; + maintainAspectRatio: boolean; + originRatio: number; +} + +export interface IUserInfoWatermarkConfig extends IGeneralWatermarkConfig, Omit { + name: boolean; + email: boolean; + phone: boolean; + uid: boolean; +} + +export interface IWatermarkConfig { + text?: ITextWatermarkConfig; + image?: IImageWatermarkConfig; + userInfo?: IUserInfoWatermarkConfig; +} + +export interface IWatermarkConfigWithType { + config: IWatermarkConfig; + type: IWatermarkTypeEnum; +} + diff --git a/packages/watermark/src/controllers/config.schema.ts b/packages/watermark/src/controllers/config.schema.ts new file mode 100644 index 00000000000..7eb5f6ee4cc --- /dev/null +++ b/packages/watermark/src/controllers/config.schema.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { MenuConfig } from '@univerjs/ui'; +import type { IImageWatermarkConfig, ITextWatermarkConfig, IUserInfoWatermarkConfig } from '../common/type'; + +export const PLUGIN_CONFIG_KEY = 'watermark.config'; + +export const configSymbol = Symbol(PLUGIN_CONFIG_KEY); + +export interface IUniverWatermarkConfig { + menu?: MenuConfig; + showMenu?: boolean; + userWatermarkSettings?: Partial; + textWatermarkSettings?: Partial; + imageWatermarkSettings?: Partial; +} + +export const defaultPluginConfig: IUniverWatermarkConfig = {}; diff --git a/packages/watermark/src/controllers/menu.schema.ts b/packages/watermark/src/controllers/menu.schema.ts new file mode 100644 index 00000000000..c743df00b70 --- /dev/null +++ b/packages/watermark/src/controllers/menu.schema.ts @@ -0,0 +1,30 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { MenuSchemaType } from '@univerjs/ui'; +import { RibbonStartGroup } from '@univerjs/ui'; +import { OpenWatermarkPanelOperation } from '../commands/operations/open-watermark-panel.operation'; +import { WatermarkMenuItemFactory } from './menu'; + +export const menuSchema: MenuSchemaType = { + [RibbonStartGroup.OTHERS]: { + [OpenWatermarkPanelOperation.id]: { + order: 0, + menuItemFactory: WatermarkMenuItemFactory, + }, + }, +}; + diff --git a/packages/watermark/src/controllers/menu.ts b/packages/watermark/src/controllers/menu.ts new file mode 100644 index 00000000000..efa6a916ced --- /dev/null +++ b/packages/watermark/src/controllers/menu.ts @@ -0,0 +1,47 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { IMenuButtonItem } from '@univerjs/ui'; +import { DOCS_ZEN_EDITOR_UNIT_ID_KEY, type IAccessor, IUniverInstanceService } from '@univerjs/core'; +import { MenuItemType } from '@univerjs/ui'; +import { of, switchMap } from 'rxjs'; +import { OpenWatermarkPanelOperation } from '../commands/operations/open-watermark-panel.operation'; +import { UNIVER_WATERMARK_MENU } from '../common/const'; + +export function WatermarkMenuItemFactory(accessor: IAccessor): IMenuButtonItem { + return { + id: OpenWatermarkPanelOperation.id, + title: 'univer-watermark.title', + tooltip: 'univer-watermark.title', + icon: UNIVER_WATERMARK_MENU, + type: MenuItemType.BUTTON, + hidden$: getWatermarkMenuHiddenObservable(accessor), + }; +} + +function getWatermarkMenuHiddenObservable(accessor: IAccessor) { + const univerInstanceService = accessor.get(IUniverInstanceService); + return univerInstanceService.focused$.pipe( + switchMap((id) => { + if (id === DOCS_ZEN_EDITOR_UNIT_ID_KEY) { + return of(true); + } else { + return of(false); + } + }) + ); +} + diff --git a/packages/watermark/src/controllers/watermark.menu.controller.ts b/packages/watermark/src/controllers/watermark.menu.controller.ts new file mode 100644 index 00000000000..aacd6b6d745 --- /dev/null +++ b/packages/watermark/src/controllers/watermark.menu.controller.ts @@ -0,0 +1,48 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Disposable, Inject } from '@univerjs/core'; +import { WatermarkSingle } from '@univerjs/icons'; +import { ComponentManager, IMenuManagerService } from '@univerjs/ui'; +import { UNIVER_WATERMARK_MENU, WATERMARK_PANEL, WATERMARK_PANEL_FOOTER } from '../common/const'; +import { WatermarkPanel } from '../views/components/WatermarkPanel'; +import { WatermarkPanelFooter } from '../views/components/WatermarkPanelFooter'; +import { menuSchema } from './menu.schema'; + +export class UniverWatermarkMenuController extends Disposable { + constructor( + @IMenuManagerService protected readonly _menuManagerService: IMenuManagerService, + @Inject(ComponentManager) private readonly _componentManager: ComponentManager + ) { + super(); + this._initMenu(); + this._initComponents(); + } + + private _initComponents() { + ([ + [UNIVER_WATERMARK_MENU, WatermarkSingle], + [WATERMARK_PANEL, WatermarkPanel], + [WATERMARK_PANEL_FOOTER, WatermarkPanelFooter], + ] as const).forEach(([key, component]) => { + this.disposeWithMe(this._componentManager.register(key, component)); + }); + } + + private _initMenu() { + this._menuManagerService.mergeMenu(menuSchema); + } +} diff --git a/packages/watermark/src/controllers/watermark.render.controller.ts b/packages/watermark/src/controllers/watermark.render.controller.ts new file mode 100644 index 00000000000..6feaaaec1f7 --- /dev/null +++ b/packages/watermark/src/controllers/watermark.render.controller.ts @@ -0,0 +1,72 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { UnitModel } from '@univerjs/core'; +import type { IRenderContext, IRenderModule } from '@univerjs/engine-render'; +import { ILocalStorageService, Inject, RxDisposable, UserManagerService } from '@univerjs/core'; +import { UNIVER_WATERMARK_LAYER_INDEX, UNIVER_WATERMARK_STORAGE_KEY } from '../common/const'; +import { type IWatermarkConfigWithType, IWatermarkTypeEnum } from '../common/type'; +import { WatermarkService } from '../services/watermark.service'; +import { WatermarkLayer } from '../views/extensions/watermark-layer'; + +export class WatermarkRenderController extends RxDisposable implements IRenderModule { + private readonly _watermarkLayer: WatermarkLayer; + + constructor( + private readonly _context: IRenderContext, + @Inject(WatermarkService) private _watermarkService: WatermarkService, + @Inject(ILocalStorageService) private _localStorageService: ILocalStorageService, + @Inject(UserManagerService) private _userManagerService: UserManagerService + ) { + super(); + this._watermarkLayer = new WatermarkLayer(_context.scene, [], UNIVER_WATERMARK_LAYER_INDEX); + this._initAddRender(); + this._initWatermarkUpdate(); + this._initWatermarkConfig(); + } + + private _initAddRender() { + const { scene } = this._context; + scene.addLayer(this._watermarkLayer); + } + + private async _initWatermarkConfig() { + const config = await this._localStorageService.getItem(UNIVER_WATERMARK_STORAGE_KEY); + if (config) { + this._watermarkService.updateWatermarkConfig(config); + this._context.mainComponent?.makeDirty(); + } + } + + private _initWatermarkUpdate() { + this.disposeWithMe( + this._watermarkService.updateConfig$.subscribe((_config) => { + if (!_config) { + this._watermarkLayer.updateConfig(); + this._context.mainComponent?.makeDirty(); + return; + } + if (_config.type === IWatermarkTypeEnum.UserInfo) { + this._watermarkLayer.updateConfig(_config, this._userManagerService.getCurrentUser()); + } else { + this._watermarkLayer.updateConfig(_config); + } + this._context.mainComponent?.makeDirty(); + }) + ); + } +} + diff --git a/packages/watermark/src/index.ts b/packages/watermark/src/index.ts new file mode 100644 index 00000000000..6b074e1dd38 --- /dev/null +++ b/packages/watermark/src/index.ts @@ -0,0 +1,21 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { UniverWatermarkPlugin } from './plugin'; +export { WatermarkImageBaseConfig, WatermarkTextBaseConfig, WatermarkUserInfoBaseConfig } from './common/const'; +export type { IImageWatermarkConfig, ITextWatermarkConfig } from './common/type'; +export { IWatermarkTypeEnum } from './common/type'; +export { WatermarkService } from './services/watermark.service'; diff --git a/packages/watermark/src/locale/en-US.ts b/packages/watermark/src/locale/en-US.ts new file mode 100644 index 00000000000..ee81ff20da4 --- /dev/null +++ b/packages/watermark/src/locale/en-US.ts @@ -0,0 +1,53 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type zhCN from './zh-CN'; + +const locale: typeof zhCN = { + 'univer-watermark': { + title: 'Watermark', + type: 'Type', + text: 'Text', + image: 'Image', + uploadImage: 'Upload Image', + replaceImage: 'Replace Image', + keepRatio: 'Keep Ratio', + width: 'Width', + height: 'Height', + style: 'Style Settings', + content: 'Content', + textPlaceholder: 'Enter text', + fontSize: 'Font Size', + direction: 'Direction', + ltr: 'Left to Right', + rtl: 'Right to Left', + inherits: 'Inherit', + opacity: 'Opacity', + layout: 'Layout Settings', + rotate: 'Rotate', + repeat: 'Repeat', + spacingX: 'Horizontal Spacing', + spacingY: 'Vertical Spacing', + startX: 'Horizontal Start Position', + startY: 'Vertical Start Position', + + cancel: 'Cancel Watermark', + close: 'Close Panel', + copy: 'Copy Config', + }, +}; + +export default locale; diff --git a/packages/watermark/src/locale/fa-IR.ts b/packages/watermark/src/locale/fa-IR.ts new file mode 100644 index 00000000000..106a8b8f0cd --- /dev/null +++ b/packages/watermark/src/locale/fa-IR.ts @@ -0,0 +1,53 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type zhCN from './zh-CN'; + +const locale: typeof zhCN = { + 'univer-watermark': { + title: 'علامت آب', + type: 'نوع', + text: 'متن', + image: 'تصویر', + uploadImage: 'آپلود تصویر', + replaceImage: 'جایگزینی تصویر', + keepRatio: 'حفظ نسبت', + width: 'عرض', + height: 'ارتفاع', + style: 'تنظیمات سبک', + content: 'محتوا', + textPlaceholder: 'متن را وارد کنید', + fontSize: 'اندازه فونت', + direction: 'جهت', + ltr: 'چپ به راست', + rtl: 'راست به چپ', + inherits: 'ارث بردن', + opacity: 'شفافیت', + layout: 'تنظیمات چیدمان', + rotate: 'چرخاندن', + repeat: 'تکرار', + spacingX: 'فاصله افقی', + spacingY: 'فاصله عمودی', + startX: 'موقعیت شروع افقی', + startY: 'موقعیت شروع عمودی', + + cancel: 'لغو علامت آب', + close: 'بستن پنل', + copy: 'کپی تنظیمات', + }, +}; + +export default locale; diff --git a/packages/watermark/src/locale/ru-RU.ts b/packages/watermark/src/locale/ru-RU.ts new file mode 100644 index 00000000000..8c2d9db5372 --- /dev/null +++ b/packages/watermark/src/locale/ru-RU.ts @@ -0,0 +1,53 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type zhCN from './zh-CN'; + +const locale: typeof zhCN = { + 'univer-watermark': { + title: 'Водяной знак', + type: 'Тип', + text: 'Текст', + image: 'Изображение', + uploadImage: 'Загрузить изображение', + replaceImage: 'Заменить изображение', + keepRatio: 'Сохранить пропорции', + width: 'Ширина', + height: 'Высота', + style: 'Настройки стиля', + content: 'Содержание', + textPlaceholder: 'Введите текст', + fontSize: 'Размер шрифта', + direction: 'Направление', + ltr: 'Слева направо', + rtl: 'Справа налево', + inherits: 'Наследовать', + opacity: 'Прозрачность', + layout: 'Настройки макета', + rotate: 'Поворот', + repeat: 'Повтор', + spacingX: 'Горизонтальный интервал', + spacingY: 'Вертикальный интервал', + startX: 'Горизонтальная начальная позиция', + startY: 'Вертикальная начальная позиция', + + cancel: 'Отменить водяной знак', + close: 'Закрыть панель', + copy: 'Копировать конфигурацию', + }, +}; + +export default locale; diff --git a/packages/watermark/src/locale/vi-VN.ts b/packages/watermark/src/locale/vi-VN.ts new file mode 100644 index 00000000000..1cd3cc1b8ee --- /dev/null +++ b/packages/watermark/src/locale/vi-VN.ts @@ -0,0 +1,53 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type zhCN from './zh-CN'; + +const locale: typeof zhCN = { + 'univer-watermark': { + title: 'Hình mờ', + type: 'Loại', + text: 'Văn bản', + image: 'Hình ảnh', + uploadImage: 'Tải lên hình ảnh', + replaceImage: 'Thay thế hình ảnh', + keepRatio: 'Giữ tỷ lệ', + width: 'Chiều rộng', + height: 'Chiều cao', + style: 'Cài đặt kiểu', + content: 'Nội dung', + textPlaceholder: 'Nhập văn bản', + fontSize: 'Kích thước phông chữ', + direction: 'Hướng', + ltr: 'Trái sang phải', + rtl: 'Phải sang trái', + inherits: 'Thừa kế', + opacity: 'Độ mờ', + layout: 'Cài đặt bố cục', + rotate: 'Xoay', + repeat: 'Lặp lại', + spacingX: 'Khoảng cách ngang', + spacingY: 'Khoảng cách dọc', + startX: 'Vị trí bắt đầu ngang', + startY: 'Vị trí bắt đầu dọc', + + cancel: 'Hủy hình mờ', + close: 'Đóng bảng điều khiển', + copy: 'Sao chép cấu hình', + }, +}; + +export default locale; diff --git a/packages/watermark/src/locale/zh-CN.ts b/packages/watermark/src/locale/zh-CN.ts new file mode 100644 index 00000000000..ff64ba8f3e1 --- /dev/null +++ b/packages/watermark/src/locale/zh-CN.ts @@ -0,0 +1,51 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const locale = { + 'univer-watermark': { + title: '水印', + type: '类型', + text: '文本', + image: '图片', + uploadImage: '上传图片', + replaceImage: '替换图片', + keepRatio: '保持比例', + width: '长度', + height: '高度', + style: '样式设置', + content: '内容', + textPlaceholder: '请输入文本', + fontSize: '字体大小', + direction: '方向', + ltr: '从左到右', + rtl: '从右到左', + inherits: '继承', + opacity: '透明度', + layout: '布局设置', + rotate: '旋转', + repeat: '重复', + spacingX: '水平间距', + spacingY: '垂直间距', + startX: '水平起始位置', + startY: '垂直起始位置', + + cancel: '取消水印', + close: '关闭面板', + copy: '复制配置', + }, +}; + +export default locale; diff --git a/packages/watermark/src/locale/zh-TW.ts b/packages/watermark/src/locale/zh-TW.ts new file mode 100644 index 00000000000..e0d6c3a200f --- /dev/null +++ b/packages/watermark/src/locale/zh-TW.ts @@ -0,0 +1,53 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type zhCN from './zh-CN'; + +const locale: typeof zhCN = { + 'univer-watermark': { + title: '浮水印', + type: '類型', + text: '文字', + image: '圖片', + uploadImage: '上傳圖片', + replaceImage: '替換圖片', + keepRatio: '保持比例', + width: '長度', + height: '高度', + style: '樣式設置', + content: '內容', + textPlaceholder: '請輸入文字', + fontSize: '字體大小', + direction: '方向', + ltr: '從左到右', + rtl: '從右到左', + inherits: '繼承', + opacity: '透明度', + layout: '佈局設置', + rotate: '旋轉', + repeat: '重複', + spacingX: '水平間距', + spacingY: '垂直間距', + startX: '水平起始位置', + startY: '垂直起始位置', + + cancel: '取消浮水印', + close: '關閉面板', + copy: '複製配置', + }, +}; + +export default locale; diff --git a/packages/watermark/src/plugin.ts b/packages/watermark/src/plugin.ts new file mode 100644 index 00000000000..229c9b54a88 --- /dev/null +++ b/packages/watermark/src/plugin.ts @@ -0,0 +1,103 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Dependency } from '@univerjs/core'; +import type { IWatermarkConfigWithType } from './common/type'; +import type { IUniverWatermarkConfig } from './controllers/config.schema'; +import { ICommandService, IConfigService, ILocalStorageService, Inject, Injector, Plugin, UniverInstanceType } from '@univerjs/core'; +import { IRenderManagerService } from '@univerjs/engine-render'; +import { OpenWatermarkPanelOperation } from './commands/operations/open-watermark-panel.operation'; +import { UNIVER_WATERMARK_STORAGE_KEY, WatermarkImageBaseConfig, WatermarkTextBaseConfig, WatermarkUserInfoBaseConfig } from './common/const'; +import { IWatermarkTypeEnum } from './common/type'; +import { UniverWatermarkMenuController } from './controllers/watermark.menu.controller'; +import { WatermarkRenderController } from './controllers/watermark.render.controller'; +import { WatermarkService } from './services/watermark.service'; + +const PLUGIN_NAME = 'UNIVER_WATERMARK_PLUGIN'; + +export class UniverWatermarkPlugin extends Plugin { + static override pluginName = PLUGIN_NAME; + + constructor( + private readonly _config: Partial = {}, + @Inject(Injector) protected override _injector: Injector, + @IConfigService private readonly _configService: IConfigService, + @Inject(ICommandService) private readonly _commandService: ICommandService, + @IRenderManagerService private readonly _renderManagerSrv: IRenderManagerService, + @Inject(ILocalStorageService) private readonly _localStorageService: ILocalStorageService + ) { + super(); + + const { menu } = this._config; + if (menu) { + this._configService.setConfig('menu', menu, { merge: true }); + } + + this._initWatermarkStorage(); + this._initDependencies(); + this._initRegisterCommand(); + } + + private async _initWatermarkStorage() { + const { menu, ...rest } = this._config; + if (rest.userWatermarkSettings) { + this._localStorageService.setItem(UNIVER_WATERMARK_STORAGE_KEY, { type: IWatermarkTypeEnum.UserInfo, config: { userInfo: { ...WatermarkUserInfoBaseConfig, ...rest.userWatermarkSettings } } }); + } else if (rest.textWatermarkSettings) { + this._localStorageService.setItem(UNIVER_WATERMARK_STORAGE_KEY, { type: IWatermarkTypeEnum.Text, config: { text: { ...WatermarkTextBaseConfig, ...rest.textWatermarkSettings } } }); + } else if (rest.imageWatermarkSettings) { + this._localStorageService.setItem(UNIVER_WATERMARK_STORAGE_KEY, { type: IWatermarkTypeEnum.Image, config: { image: { ...WatermarkImageBaseConfig, ...rest.imageWatermarkSettings } } }); + } else { + const config = await this._localStorageService.getItem(UNIVER_WATERMARK_STORAGE_KEY); + if (config?.type === IWatermarkTypeEnum.UserInfo) { + this._localStorageService.removeItem(UNIVER_WATERMARK_STORAGE_KEY); + } + } + } + + private _initDependencies(): void { + ([[WatermarkService], [UniverWatermarkMenuController]] as Dependency[]).forEach((d) => { + this._injector.add(d); + }); + } + + private _initRegisterCommand(): void { + [ + OpenWatermarkPanelOperation, + ].forEach((m) => this._commandService.registerCommand(m)); + } + + override onRendered(): void { + const injector = this._injector; + injector.get(WatermarkService); + + const { userWatermarkSettings, textWatermarkSettings, imageWatermarkSettings, showMenu = true } = this._config; + const shouldDisplayUI = !(userWatermarkSettings || textWatermarkSettings || imageWatermarkSettings) && showMenu; + if (shouldDisplayUI) { + injector.get(UniverWatermarkMenuController); + } + + this._initRenderDependencies(); + } + + private _initRenderDependencies(): void { + ([ + [WatermarkRenderController], + ] as Dependency[]).forEach((d) => { + this._renderManagerSrv.registerRenderModule(UniverInstanceType.UNIVER_SHEET, d); + this._renderManagerSrv.registerRenderModule(UniverInstanceType.UNIVER_DOC, d); + }); + } +} diff --git a/packages/watermark/src/services/watermark.service.ts b/packages/watermark/src/services/watermark.service.ts new file mode 100644 index 00000000000..b9ea61932a9 --- /dev/null +++ b/packages/watermark/src/services/watermark.service.ts @@ -0,0 +1,58 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Nullable } from '@univerjs/core'; +import type { IWatermarkConfigWithType } from '../common/type'; +import { Disposable, ILocalStorageService, Inject } from '@univerjs/core'; +import { Subject } from 'rxjs'; +import { UNIVER_WATERMARK_STORAGE_KEY } from '../common/const'; + +export class WatermarkService extends Disposable { + private readonly _updateConfig$ = new Subject>(); + public readonly updateConfig$ = this._updateConfig$.asObservable(); + private readonly _refresh$ = new Subject(); + public readonly refresh$ = this._refresh$.asObservable(); + + constructor( + @Inject(ILocalStorageService) private _localStorageService: ILocalStorageService + ) { + super(); + } + + public async getWatermarkConfig() { + const res = await this._localStorageService.getItem(UNIVER_WATERMARK_STORAGE_KEY); + return res; + } + + public updateWatermarkConfig(config: IWatermarkConfigWithType) { + this._localStorageService.setItem(UNIVER_WATERMARK_STORAGE_KEY, config); + this._updateConfig$.next(config); + } + + public deleteWatermarkConfig() { + this._localStorageService.removeItem(UNIVER_WATERMARK_STORAGE_KEY); + this._updateConfig$.next(null); + } + + public refresh() { + this._refresh$.next(Math.random()); + } + + override dispose(): void { + this._refresh$.complete(); + this._updateConfig$.complete(); + } +} diff --git a/packages/watermark/src/util.ts b/packages/watermark/src/util.ts new file mode 100644 index 00000000000..66f7fba63bf --- /dev/null +++ b/packages/watermark/src/util.ts @@ -0,0 +1,175 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { IUser, Nullable } from '@univerjs/core'; +import type { UniverRenderingContext } from '@univerjs/engine-render'; +import type { IImageWatermarkConfig, ITextWatermarkConfig, IUserInfoWatermarkConfig, IWatermarkConfigWithType } from './common/type'; +import { IWatermarkTypeEnum } from './common/type'; + +export function renderWatermark(ctx: UniverRenderingContext, config: IWatermarkConfigWithType, image: Nullable, userInfo: Nullable) { + const type = config.type; + const watermarkConfig = config.config; + if (type === IWatermarkTypeEnum.UserInfo && watermarkConfig.userInfo) { + renderUserInfoWatermark(ctx, watermarkConfig.userInfo, userInfo); + } else if (type === IWatermarkTypeEnum.Image && watermarkConfig.image) { + renderImageWatermark(ctx, watermarkConfig.image, image); + } else if (type === IWatermarkTypeEnum.Text && watermarkConfig.text) { + renderTextWatermark(ctx, watermarkConfig.text); + } +} + +export function renderUserInfoWatermark(ctx: UniverRenderingContext, config: IUserInfoWatermarkConfig, userInfo: Nullable) { + const { x, y, repeat, spacingX, spacingY, rotate, opacity, name, fontSize, color, bold, italic, direction } = config; + + if (!userInfo) { + return; + } + + let watermarkContent = ''; + if (name) { + watermarkContent += `${userInfo.name} `; + } + + // if (email) { + // watermarkContent += `Email: ${userInfo.email} `; + // } + // if (phone) { + // watermarkContent += `Phone: ${userInfo.phone} `; + // } + // if (uid) { + // watermarkContent += `UID: ${userInfo.uid} `; + // } + + if (!watermarkContent) { + return; + } + + ctx.save(); + ctx.globalAlpha = opacity; + + ctx.direction = direction; + + let fontStyle = ''; + if (italic) fontStyle += 'italic '; + if (bold) fontStyle += 'bold '; + fontStyle += `${fontSize}px Arial`; + ctx.font = fontStyle; + ctx.fillStyle = color; + + if (repeat) { + // Draw repeated text across the canvas + const canvasWidth = ctx.canvas.width; + const canvasHeight = ctx.canvas.height; + + for (let posY = y; posY < canvasHeight; posY += fontSize + spacingY) { + for (let posX = x; posX < canvasWidth; posX += ctx.measureText(watermarkContent).width + spacingX) { + ctx.save(); + ctx.translate(posX, posY); + ctx.rotate((Math.PI / 180) * rotate); + ctx.fillText(watermarkContent, 0, 0); + ctx.restore(); + } + } + } else { + ctx.save(); + ctx.translate(x, y); + ctx.rotate((Math.PI / 180) * rotate); + ctx.fillText(watermarkContent, 0, 0); + ctx.restore(); + } + + ctx.restore(); +} + +export function renderTextWatermark(ctx: UniverRenderingContext, config: ITextWatermarkConfig) { + const { x, y, repeat, spacingX, spacingY, rotate, opacity, content, fontSize, color, bold, italic, direction } = config; + + ctx.save(); + ctx.globalAlpha = opacity; + + ctx.direction = direction; + + let fontStyle = ''; + if (italic) fontStyle += 'italic '; + if (bold) fontStyle += 'bold '; + fontStyle += `${fontSize}px Arial`; + ctx.font = fontStyle; + ctx.fillStyle = color; + + if (content) { + if (repeat) { + // Draw repeated text across the canvas + const canvasWidth = ctx.canvas.width; + const canvasHeight = ctx.canvas.height; + + for (let posY = y; posY < canvasHeight; posY += fontSize + spacingY) { + for (let posX = x; posX < canvasWidth; posX += ctx.measureText(content).width + spacingX) { + ctx.save(); + ctx.translate(posX, posY); + ctx.rotate((Math.PI / 180) * rotate); + ctx.fillText(content, 0, 0); + ctx.restore(); + } + } + } else { + ctx.save(); + ctx.translate(x, y); + ctx.rotate((Math.PI / 180) * rotate); + ctx.fillText(content, 0, 0); + ctx.restore(); + } + } + + ctx.restore(); +} + +export function renderImageWatermark(ctx: UniverRenderingContext, config: IImageWatermarkConfig, image: Nullable) { + const { x, y, repeat, spacingX, spacingY, rotate, opacity, width, height, maintainAspectRatio, originRatio } = config; + + if (!image?.complete) { + return; + } + + ctx.save(); + ctx.globalAlpha = opacity; + + const actualWidth = maintainAspectRatio ? width : width; + const actualHeight = maintainAspectRatio ? width / originRatio : height; + + if (repeat) { + const canvasWidth = ctx.canvas.width; + const canvasHeight = ctx.canvas.height; + + for (let posY = y; posY < canvasHeight; posY += actualHeight + spacingY) { + for (let posX = x; posX < canvasWidth; posX += actualWidth + spacingX) { + ctx.save(); + ctx.translate(posX, posY); + ctx.rotate((Math.PI / 180) * rotate); + ctx.drawImage(image, 0, 0, actualWidth, actualHeight); + ctx.restore(); + } + } + } else { + ctx.save(); + ctx.translate(x, y); + ctx.rotate((Math.PI / 180) * rotate); + ctx.drawImage(image, 0, 0, actualWidth, actualHeight); + ctx.restore(); + } + + ctx.restore(); +}; + diff --git a/packages/watermark/src/views/components/WatermarkImageSetting.tsx b/packages/watermark/src/views/components/WatermarkImageSetting.tsx new file mode 100644 index 00000000000..11b236b2039 --- /dev/null +++ b/packages/watermark/src/views/components/WatermarkImageSetting.tsx @@ -0,0 +1,249 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { IImageWatermarkConfig } from '../../common/type'; +import { LocaleService, useDependency } from '@univerjs/core'; +import { Button, Checkbox, InputNumber } from '@univerjs/design'; +import { ILocalFileService } from '@univerjs/ui'; +import React from 'react'; +import { WATERMARK_IMAGE_ALLOW_IMAGE_LIST } from '../../common/const'; +import styles from './index.module.less'; + +interface IWatermarkImageSettingProps { + config?: IImageWatermarkConfig; + onChange: (config: IImageWatermarkConfig) => void; +} + +export const WatermarkImageSetting: React.FC = ({ config, onChange }) => { + const fileOpenService = useDependency(ILocalFileService); + const localeService = useDependency(LocaleService); + + if (!config) return null; + + const handleUpdateImageUrl = async () => { + const files = await fileOpenService.openFile({ + multiple: false, + accept: WATERMARK_IMAGE_ALLOW_IMAGE_LIST.map((image) => `.${image.replace('image/', '')}`).join(','), + }); + + const fileLength = files.length; + if (fileLength === 0) { + return false; + } + + const file = files[0]; + + const reader = new FileReader(); + + reader.onload = function (event) { + if (event.target?.result) { + const base64String = event.target.result; + + const img = new Image(); + img.onload = function () { + onChange({ ...config, url: base64String as string, width: Math.max(20, img.width), height: Math.max(img.height, 20), originRatio: img.width / img.height }); + }; + + img.src = base64String as string; + } + }; + + reader.readAsDataURL(file); + }; + + return ( +
+ +
{localeService.t('univer-watermark.image')}
+ +
+ {localeService.t('univer-watermark.image')} + +
+
+
{localeService.t('univer-watermark.opacity')}
+ { + if (val != null) { + onChange({ ...config, opacity: Number.parseFloat(val.toString()) }); + } + }} + max={1} + min={0} + step={0.05} + className={styles.watermarkInput} + /> +
+ +
+
{localeService.t('univer-watermark.keepRatio')}
+ { + if (val === true) { + onChange({ ...config, maintainAspectRatio: val as boolean, height: Math.round(config.width / config.originRatio) }); + } else { + onChange({ ...config, maintainAspectRatio: val as boolean }); + } + }} + > + + +
+
+
+ +
+
+
+
{localeService.t('univer-watermark.width')}
+ { + if (val != null) { + const newWidth = Math.max(20, Number.parseInt(val.toString())); + if (config.maintainAspectRatio) { + onChange({ ...config, width: newWidth, height: Math.round(newWidth / config.originRatio) }); + } else { + onChange({ ...config, width: newWidth }); + } + } + }} + min={20} + className={styles.watermarkInput} + /> +
+ +
+
{localeService.t('univer-watermark.height')}
+ { + if (val != null) { + const newHeight = Math.max(20, Number.parseInt(val.toString())); + if (config.maintainAspectRatio) { + onChange({ ...config, height: newHeight, width: Math.round(newHeight * config.originRatio) }); + } else { + onChange({ ...config, height: Number.parseInt(val.toString()) }); + } + } + }} + min={20} + className={styles.watermarkInput} + /> +
+
+
+ +
{localeService.t('univer-watermark.layout')}
+ +
+ +
+
+
{localeService.t('univer-watermark.rotate')}
+ { + if (val != null) { + onChange({ ...config, rotate: Number.parseInt(val.toString()) }); + } + }} + max={360} + min={-360} + className={styles.watermarkInput} + /> +
+
+
{localeService.t('univer-watermark.repeat')}
+ onChange({ ...config, repeat: val as boolean })} + > + + +
+
+
+
+
{localeService.t('univer-watermark.spacingX')}
+ { + if (val != null) { + onChange({ ...config, spacingX: Number.parseInt(val.toString()) }); + } + }} + min={0} + className={styles.watermarkInput} + /> +
+ +
+
{localeService.t('univer-watermark.spacingY')}
+ { + if (val != null) { + onChange({ ...config, spacingY: Number.parseInt(val.toString()) }); + } + }} + min={0} + className={styles.watermarkInput} + /> +
+
+
+ +
+
{localeService.t('univer-watermark.startX')}
+ { + if (val != null) { + onChange({ ...config, x: Number.parseInt(val.toString()) }); + } + }} + min={0} + className={styles.watermarkInput} + /> +
+ +
+
{localeService.t('univer-watermark.startY')}
+ { + if (val != null) { + onChange({ ...config, y: Number.parseInt(val.toString()) }); + } + }} + min={0} + className={styles.watermarkInput} + /> +
+
+
+ +
+ ); +}; diff --git a/packages/watermark/src/views/components/WatermarkPanel.tsx b/packages/watermark/src/views/components/WatermarkPanel.tsx new file mode 100644 index 00000000000..2f55aef18a0 --- /dev/null +++ b/packages/watermark/src/views/components/WatermarkPanel.tsx @@ -0,0 +1,82 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { IWatermarkConfig, IWatermarkConfigWithType } from '../../common/type'; +import { ILocalStorageService, LocaleService, useDependency, useObservable } from '@univerjs/core'; +import { Select } from '@univerjs/design'; +import React, { useCallback, useEffect, useState } from 'react'; +import { UNIVER_WATERMARK_STORAGE_KEY, WatermarkImageBaseConfig, WatermarkTextBaseConfig } from '../../common/const'; +import { IWatermarkTypeEnum } from '../../common/type'; +import { WatermarkService } from '../../services/watermark.service'; +import styles from './index.module.less'; +import { WatermarkImageSetting } from './WatermarkImageSetting'; +import { WatermarkTextSetting } from './WatermarkTextSetting'; + +export const WatermarkPanel: React.FC = () => { + const [watermarkType, setWatermarkType] = useState(IWatermarkTypeEnum.Text); + const [config, setConfig] = useState(); + const watermarkService = useDependency(WatermarkService); + const localStorageService = useDependency(ILocalStorageService); + const _refresh = useObservable(watermarkService.refresh$); + const localeService = useDependency(LocaleService); + + function handleConfigChange(config: IWatermarkConfig, type?: IWatermarkTypeEnum) { + setConfig(config); + watermarkService.updateWatermarkConfig({ type: type ?? watermarkType, config }); + } + + const getWatermarkConfig = useCallback(async () => { + const watermarkConfig = await localStorageService.getItem(UNIVER_WATERMARK_STORAGE_KEY); + if (watermarkConfig) { + setWatermarkType(watermarkConfig.type); + setConfig(watermarkConfig.config); + } else { + setConfig({ text: WatermarkTextBaseConfig }); + } + }, []); + + useEffect(() => { + getWatermarkConfig(); + }, [_refresh, getWatermarkConfig]); + + return ( +
+
{localeService.t('univer-watermark.type')}
+ +
+ {watermarkType === IWatermarkTypeEnum.Text && handleConfigChange({ text: v })} />} + {watermarkType === IWatermarkTypeEnum.Image && handleConfigChange({ image: v })} />} +
+
+ ); +}; + diff --git a/packages/watermark/src/views/components/WatermarkPanelFooter.tsx b/packages/watermark/src/views/components/WatermarkPanelFooter.tsx new file mode 100644 index 00000000000..aabb9c14b11 --- /dev/null +++ b/packages/watermark/src/views/components/WatermarkPanelFooter.tsx @@ -0,0 +1,78 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LocaleService, useDependency } from '@univerjs/core'; +import { Button } from '@univerjs/design'; +import { IClipboardInterfaceService, ISidebarService } from '@univerjs/ui'; +import React from 'react'; +import { WatermarkTextBaseConfig } from '../../common/const'; +import { IWatermarkTypeEnum } from '../../common/type'; +import { WatermarkService } from '../../services/watermark.service'; +import styles from './index.module.less'; + +export const WatermarkPanelFooter: React.FC = () => { + const sidebarService = useDependency(ISidebarService); + const watermarkService = useDependency(WatermarkService); + const localeService = useDependency(LocaleService); + const clipboardService = useDependency(IClipboardInterfaceService); + + return ( +
+
{ + watermarkService.updateWatermarkConfig({ + type: IWatermarkTypeEnum.Text, + config: { text: WatermarkTextBaseConfig }, + }); + watermarkService.refresh(); + }} + > + {localeService.t('univer-watermark.cancel')} +
+ +
+ + +
+
+ ); +}; diff --git a/packages/watermark/src/views/components/WatermarkTextSetting.tsx b/packages/watermark/src/views/components/WatermarkTextSetting.tsx new file mode 100644 index 00000000000..4abbb831a35 --- /dev/null +++ b/packages/watermark/src/views/components/WatermarkTextSetting.tsx @@ -0,0 +1,211 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ITextWatermarkConfig } from '../../common/type'; +import { LocaleService, useDependency } from '@univerjs/core'; +import { Checkbox, ColorPicker, Dropdown, Input, InputNumber, Select } from '@univerjs/design'; +import { BoldSingle, FontColor, ItalicSingle } from '@univerjs/icons'; +import clsx from 'clsx'; +import React from 'react'; +import styles from './index.module.less'; + +interface IWatermarkTextSettingProps { + config?: ITextWatermarkConfig; + onChange: (config: ITextWatermarkConfig) => void; +} + +export const WatermarkTextSetting: React.FC = (props) => { + const { config, onChange } = props; + const localeService = useDependency(LocaleService); + + if (!config) return null; + + return ( +
+
{localeService.t('univer-watermark.style')}
+ +
+
{localeService.t('univer-watermark.content')}
+ onChange({ ...config, content: val })} + className={styles.watermarkInputContent} + placeholder={localeService.t('univer-watermark.textPlaceholder')} + > + +
+ +
+
+
+
{localeService.t('univer-watermark.fontSize')}
+ { + if (val != null) { + onChange({ ...config, fontSize: Number.parseInt(val.toString()) }); + } + }} + max={72} + min={12} + className={styles.watermarkInput} + /> +
+ +
+
{localeService.t('univer-watermark.direction')}
+ +
+ +
+
{localeService.t('univer-watermark.opacity')}
+ { + if (val != null) { + onChange({ ...config, opacity: Number.parseFloat(val.toString()) }); + } + }} + max={1} + min={0} + step={0.05} + className={styles.watermarkInput} + /> +
+
+ +
+
+ + onChange({ ...config, color: val })} /> +
+ )} + > + + +
+
{ onChange({ ...config, bold: !config.bold }); }}> + +
+
{ onChange({ ...config, italic: !config.italic }); }}> + +
+
+
+ +
{localeService.t('univer-watermark.layout')}
+ +
+ +
+
+
{localeService.t('univer-watermark.rotate')}
+ { + if (val != null) { + onChange({ ...config, rotate: Number.parseInt(val.toString()) }); + } + }} + max={360} + min={-360} + className={styles.watermarkInput} + /> +
+
+
{localeService.t('univer-watermark.repeat')}
+ onChange({ ...config, repeat: val as boolean })} + > + + +
+
+
+
+
{localeService.t('univer-watermark.spacingX')}
+ { + if (val != null) { + onChange({ ...config, spacingX: Number.parseInt(val.toString()) }); + } + }} + min={0} + className={styles.watermarkInput} + /> +
+ +
+
{localeService.t('univer-watermark.spacingY')}
+ { + if (val != null) { + onChange({ ...config, spacingY: Number.parseInt(val.toString()) }); + } + }} + min={0} + className={styles.watermarkInput} + /> +
+
+
+ +
+
{localeService.t('univer-watermark.startX')}
+ { + if (val != null) { + onChange({ ...config, x: Number.parseInt(val.toString()) }); + } + }} + min={0} + className={styles.watermarkInput} + /> +
+ +
+
{localeService.t('univer-watermark.startY')}
+ { + if (val != null) { + onChange({ ...config, y: Number.parseInt(val.toString()) }); + } + }} + min={0} + className={styles.watermarkInput} + /> +
+
+
+ + + ); +}; diff --git a/packages/watermark/src/views/components/index.module.less b/packages/watermark/src/views/components/index.module.less new file mode 100644 index 00000000000..49cb884e7f5 --- /dev/null +++ b/packages/watermark/src/views/components/index.module.less @@ -0,0 +1,127 @@ +.watermark { + &-panel { + margin: 16px 0; + + &-type-title { + font-size: 15px; + color: rgb(var(--text-color-secondary)); + } + + &-type-select { + width: 100%; + margin-top: 16px; + } + + &-setting { + width: 100%; + min-height: 100px; + border-radius: 8px; + } + + &-footer { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 12px; + + &-button-wrapper { + display: flex; + align-items: center; + } + + &-reset { + cursor: pointer; + white-space: nowrap; + color: rgb(var(--primary-color)); + border-bottom: 1px solid rgb(var(--primary-color)); + } + } + } + + &-text-setting { + font-size: 14px; + + &-header { + font-size: 15px; + color: rgb(var(--text-color-secondary)); + margin: 16px 0; + } + + &-font { + &-content { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + } + + &-style-part { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 16px; + + &:first-child { + margin-top: 0; + } + } + } + + &-layout { + &-font-wrapper { + width: calc(50% - 8px); + align-self: flex-start; + white-space: nowrap; + } + } + } + + &-image-setting { + font-size: 14px; + } + + &-input { + height: 28px; + box-sizing: border-box; + + &-content { + width: 220px; + } + } + + &-icon { + width: 18px; + height: 18px; + margin: 2px; + + &-wrapper { + cursor: pointer; + border-radius: 4px; + width: 24px; + height: 24px; + + &-select { + background-color: rgb(var(--grey-100)); + } + + &:hover { + background-color: rgb(var(--grey-100)); + } + + &:first-child { + margin-left: 28px; + } + + &:last-child { + margin-right: 28px; + } + } + } + + &-color-picker-wrapper { + padding: 16px; + border-radius: 8px; + background-color: #fff; + border: 1px solid rgb(var(--grey-100)); + } +} diff --git a/packages/watermark/src/views/extensions/watermark-layer.ts b/packages/watermark/src/views/extensions/watermark-layer.ts new file mode 100644 index 00000000000..3504176b001 --- /dev/null +++ b/packages/watermark/src/views/extensions/watermark-layer.ts @@ -0,0 +1,53 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { IUser, Nullable } from '@univerjs/core'; +import type { UniverRenderingContext } from '@univerjs/engine-render'; +import { Layer } from '@univerjs/engine-render'; +import { type IWatermarkConfigWithType, IWatermarkTypeEnum } from '../../common/type'; +import { renderWatermark } from '../../util'; + +export class WatermarkLayer extends Layer { + private _config: Nullable; + private _image: Nullable; + private _user: Nullable; + + override render(ctx?: UniverRenderingContext, isMaxLayer = false) { + super.render(ctx, isMaxLayer); + const mainCtx = ctx || this.scene.getEngine()?.getCanvas().getContext(); + if (mainCtx && mainCtx.getId()) { + this._renderWatermark(mainCtx); + } + return this; + } + + public updateConfig(config?: IWatermarkConfigWithType, user?: IUser) { + this._config = config; + if (this._config?.type === IWatermarkTypeEnum.Image && this._config.config.image) { + this._image = new Image(); + this._image.src = this._config.config.image.url; + } + if (user) { + this._user = user; + } + } + + private _renderWatermark(ctx: UniverRenderingContext) { + if (this._config) { + renderWatermark(ctx, this._config, this._image, this._user); + } + } +} diff --git a/packages/watermark/src/vite-env.d.ts b/packages/watermark/src/vite-env.d.ts new file mode 100644 index 00000000000..11f02fe2a00 --- /dev/null +++ b/packages/watermark/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/watermark/tsconfig.json b/packages/watermark/tsconfig.json new file mode 100644 index 00000000000..c62210cbad2 --- /dev/null +++ b/packages/watermark/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@univerjs-infra/shared/tsconfigs/base", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "references": [{ "path": "./tsconfig.node.json" }], + "include": ["src"] +} diff --git a/packages/watermark/tsconfig.node.json b/packages/watermark/tsconfig.node.json new file mode 100644 index 00000000000..50ef1e6862b --- /dev/null +++ b/packages/watermark/tsconfig.node.json @@ -0,0 +1,4 @@ +{ + "extends": "@univerjs-infra/shared/tsconfigs/node", + "include": ["vite.config.ts"] +} diff --git a/packages/watermark/vite.config.ts b/packages/watermark/vite.config.ts new file mode 100644 index 00000000000..ff0e0966ce6 --- /dev/null +++ b/packages/watermark/vite.config.ts @@ -0,0 +1,12 @@ +import createViteConfig from '@univerjs-infra/shared/vite'; +import pkg from './package.json'; + +export default ({ mode }) => createViteConfig({}, { + mode, + pkg, + features: { + react: true, + css: true, + dom: true, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d448d6cd3f..3715f40298c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,7 +28,7 @@ importers: version: 4.2.0(release-it@17.8.2(typescript@5.6.3)) '@release-it/conventional-changelog': specifier: ^8.0.2 - version: 8.0.2(release-it@17.8.2(typescript@5.6.3)) + version: 8.0.2(conventional-commits-filter@4.0.0)(conventional-commits-parser@5.0.0)(release-it@17.8.2(typescript@5.6.3)) '@storybook/react': specifier: 8.3.5 version: 8.3.5(@storybook/test@8.3.5(storybook@8.3.5))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.5)(typescript@5.6.3) @@ -360,6 +360,9 @@ importers: '@univerjs/uniui': specifier: workspace:* version: link:../packages-experimental/uniui + '@univerjs/watermark': + specifier: workspace:* + version: link:../packages/watermark clsx: specifier: ^2.1.1 version: 2.1.1 @@ -1689,6 +1692,9 @@ importers: '@univerjs/ui': specifier: workspace:* version: link:../ui + '@univerjs/watermark': + specifier: workspace:* + version: link:../watermark devDependencies: '@univerjs-infra/shared': specifier: workspace:* @@ -3390,6 +3396,52 @@ importers: specifier: ^2.1.2 version: 2.1.2(@types/node@22.7.5)(happy-dom@15.0.0)(jsdom@24.1.1)(less@4.2.0)(sass@1.77.5)(terser@5.31.6) + packages/watermark: + dependencies: + '@univerjs/core': + specifier: workspace:* + version: link:../core + '@univerjs/design': + specifier: workspace:* + version: link:../design + '@univerjs/engine-render': + specifier: workspace:* + version: link:../engine-render + '@univerjs/icons': + specifier: ^0.1.80 + version: 0.1.80(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@univerjs/ui': + specifier: workspace:* + version: link:../ui + clsx: + specifier: ^2.1.1 + version: 2.1.1 + devDependencies: + '@types/react': + specifier: ^18.3.9 + version: 18.3.11 + '@univerjs-infra/shared': + specifier: workspace:* + version: link:../../common/shared + less: + specifier: ^4.2.0 + version: 4.2.0 + react: + specifier: 18.3.1 + version: 18.3.1 + rxjs: + specifier: ^7.8.1 + version: 7.8.1 + typescript: + specifier: ^5.6.2 + version: 5.6.3 + vite: + specifier: ^5.4.8 + version: 5.4.8(@types/node@22.7.5)(less@4.2.0)(sass@1.77.5)(terser@5.31.6) + vitest: + specifier: ^2.1.1 + version: 2.1.2(@types/node@22.7.5)(happy-dom@15.0.0)(jsdom@24.1.1)(less@4.2.0)(sass@1.77.5)(terser@5.31.6) + packages: '@adobe/css-tools@4.4.0': @@ -5175,6 +5227,12 @@ packages: react: '*' react-dom: '*' + '@univerjs/icons@0.1.80': + resolution: {integrity: sha512-6lR/dhGkGWbahgpdDdf/xEpDJxcRllIQTnknR9pNyo+4VdO/QrVaI3a2NgebY+0VQTD2rX5CuKKJhbkAdzXQdA==} + peerDependencies: + react: '*' + react-dom: '*' + '@univerjs/protocol@0.1.39-alpha.30': resolution: {integrity: sha512-mgA8MvuYTWc+E5Fedi45oLKMDBjFzEwOLpFSSZrvzA+rqRnXeVZY21wSOZPHSb+GlIKdNxVtdhg3/ahhVaKJvA==} engines: {node: '>=16.0.0', npm: '>=8.0.0'} @@ -10824,10 +10882,13 @@ snapshots: '@types/conventional-commits-parser': 5.0.0 chalk: 5.3.0 - '@conventional-changelog/git-client@1.0.1': + '@conventional-changelog/git-client@1.0.1(conventional-commits-filter@4.0.0)(conventional-commits-parser@5.0.0)': dependencies: '@types/semver': 7.5.8 semver: 7.6.3 + optionalDependencies: + conventional-commits-filter: 4.0.0 + conventional-commits-parser: 5.0.0 '@dprint/formatter@0.3.0': {} @@ -11523,12 +11584,12 @@ snapshots: walk-sync: 2.2.0 yaml: 2.4.5 - '@release-it/conventional-changelog@8.0.2(release-it@17.8.2(typescript@5.6.3))': + '@release-it/conventional-changelog@8.0.2(conventional-commits-filter@4.0.0)(conventional-commits-parser@5.0.0)(release-it@17.8.2(typescript@5.6.3))': dependencies: concat-stream: 2.0.0 conventional-changelog: 5.1.0 conventional-recommended-bump: 9.0.0 - git-semver-tags: 8.0.0 + git-semver-tags: 8.0.0(conventional-commits-filter@4.0.0)(conventional-commits-parser@5.0.0) release-it: 17.8.2(typescript@5.6.3) semver: 7.6.3 transitivePeerDependencies: @@ -11984,8 +12045,8 @@ snapshots: dependencies: '@typescript-eslint/utils': 8.7.0(eslint@9.12.0(jiti@1.21.6))(typescript@5.6.3) eslint: 9.12.0(jiti@1.21.6) - eslint-visitor-keys: 4.0.0 - espree: 10.1.0 + eslint-visitor-keys: 4.1.0 + espree: 10.2.0 estraverse: 5.3.0 picomatch: 4.0.2 transitivePeerDependencies: @@ -12394,6 +12455,11 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@univerjs/icons@0.1.80(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@univerjs/protocol@0.1.39-alpha.30(@grpc/grpc-js@1.10.9)(rxjs@7.8.1)': dependencies: '@grpc/grpc-js': 1.10.9 @@ -14859,9 +14925,9 @@ snapshots: meow: 12.1.1 semver: 7.6.3 - git-semver-tags@8.0.0: + git-semver-tags@8.0.0(conventional-commits-filter@4.0.0)(conventional-commits-parser@5.0.0): dependencies: - '@conventional-changelog/git-client': 1.0.1 + '@conventional-changelog/git-client': 1.0.1(conventional-commits-filter@4.0.0)(conventional-commits-parser@5.0.0) meow: 13.2.0 transitivePeerDependencies: - conventional-commits-filter @@ -15580,7 +15646,7 @@ snapshots: dependencies: copy-anything: 2.0.6 parse-node-version: 1.0.1 - tslib: 2.6.3 + tslib: 2.7.0 optionalDependencies: errno: 0.1.8 graceful-fs: 4.2.11