RED-6014: show japanese characters in watermark text

This commit is contained in:
Dan Percic 2023-02-06 18:29:27 +02:00
parent ff32854e09
commit 528647146c
7 changed files with 104 additions and 116 deletions

View File

@ -1,8 +1,8 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core';
import { PreferencesKeys, UserPreferenceService } from '@users/user-preference.service'; import { PreferencesKeys, UserPreferenceService } from '@users/user-preference.service';
import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; import { FormBuilder, FormGroup } from '@angular/forms';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { BaseFormComponent, IqserPermissionsService } from '@iqser/common-ui'; import { AsControl, BaseFormComponent, IqserPermissionsService } from '@iqser/common-ui';
import { ROLES } from '@users/roles'; import { ROLES } from '@users/roles';
interface PreferencesForm { interface PreferencesForm {
@ -16,7 +16,6 @@ interface PreferencesForm {
[k: string]: any; [k: string]: any;
} }
type AsControl<T> = { [K in keyof T]: FormControl<T[K]> };
type Screen = 'preferences' | 'warnings-preferences'; type Screen = 'preferences' | 'warnings-preferences';
const Screens = { const Screens = {

View File

@ -1,5 +1,6 @@
<div class="content-container"> <div class="content-container">
<div #viewer class="viewer"></div> <div class="viewer" id="viewer"></div>
<div *ngIf="changed && currentUser.isAdmin" class="changes-box"> <div *ngIf="changed && currentUser.isAdmin" class="changes-box">
<iqser-icon-button <iqser-icon-button
(action)="save()" (action)="save()"

View File

@ -1,8 +1,9 @@
import { ChangeDetectorRef, Component, ElementRef, Inject, ViewChild } from '@angular/core'; import { ChangeDetectorRef, Component, Inject } from '@angular/core';
import WebViewer, { WebViewerInstance } from '@pdftron/webviewer'; import WebViewer, { WebViewerInstance } from '@pdftron/webviewer';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { FormBuilder, FormGroup } from '@angular/forms';
import { import {
AsControl,
BASE_HREF_FN, BASE_HREF_FN,
BaseHrefFn, BaseHrefFn,
Debounce, Debounce,
@ -35,6 +36,16 @@ export const DEFAULT_WATERMARK: Partial<IWatermark> = {
orientation: WatermarkOrientations.HORIZONTAL, orientation: WatermarkOrientations.HORIZONTAL,
} as const; } as const;
interface WatermarkForm {
name: string;
text: string;
hexColor: string;
opacity: number;
fontSize: number;
fontType: string;
orientation: WatermarkOrientation;
}
@Component({ @Component({
selector: 'redaction-watermark-screen', selector: 'redaction-watermark-screen',
templateUrl: './watermark-screen.component.html', templateUrl: './watermark-screen.component.html',
@ -49,13 +60,11 @@ export class WatermarkScreenComponent {
readonly #watermarkId = Number(getParam(WATERMARK_ID)); readonly #watermarkId = Number(getParam(WATERMARK_ID));
private _instance: WebViewerInstance; private _instance: WebViewerInstance;
private _watermark: Partial<IWatermark> = {}; private _watermark: Partial<IWatermark> = {};
@ViewChild('viewer', { static: true })
private _viewer: ElementRef;
constructor( constructor(
private readonly _http: HttpClient, private readonly _http: HttpClient,
private readonly _toaster: Toaster, private readonly _toaster: Toaster,
private readonly _formBuilder: UntypedFormBuilder, private readonly _formBuilder: FormBuilder,
readonly permissionsService: IqserPermissionsService, readonly permissionsService: IqserPermissionsService,
private readonly _loadingService: LoadingService, private readonly _loadingService: LoadingService,
private readonly _licenseService: LicenseService, private readonly _licenseService: LicenseService,
@ -82,7 +91,7 @@ export class WatermarkScreenComponent {
} }
get valid(): boolean { get valid(): boolean {
if (!this.form.get('name')?.value || !this.form.get('text')?.value) { if (!this.form.controls.name?.value || !this.form.controls.text?.value) {
return false; return false;
} }
return this.form.valid; return this.form.valid;
@ -102,22 +111,13 @@ export class WatermarkScreenComponent {
}; };
this._loadingService.start(); this._loadingService.start();
try { try {
await firstValueFrom( const updatedWatermark = await this._watermarkService.saveWatermark(watermark);
this._watermarkService.saveWatermark(watermark).pipe(
tap(() => {
this._toaster.success( this._toaster.success(
watermark.id ? _('watermark-screen.action.change-success') : _('watermark-screen.action.created-success'), watermark.id ? _('watermark-screen.action.change-success') : _('watermark-screen.action.created-success'),
); );
}),
tap(async updatedWatermark => {
if (!watermark.id) { if (!watermark.id) {
await this._router.navigate([ await this._router.navigate([`/main/admin/dossier-templates/${this.#dossierTemplateId}/watermarks/${updatedWatermark.id}`]);
`/main/admin/dossier-templates/${this.#dossierTemplateId}/watermarks/${updatedWatermark.id}`,
]);
} }
}),
),
);
} catch (error) { } catch (error) {
this._toaster.error(_('watermark-screen.action.error')); this._toaster.error(_('watermark-screen.action.error'));
} }
@ -136,16 +136,19 @@ export class WatermarkScreenComponent {
} }
} }
private _initForm(watermark: Partial<IWatermark>) { private async _initForm(watermark: Partial<IWatermark>) {
this._watermark = { ...watermark, dossierTemplateId: this.#dossierTemplateId }; this._watermark = { ...watermark, dossierTemplateId: this.#dossierTemplateId };
this.form.patchValue({ ...watermark }); this.form.patchValue({ ...watermark });
this._loadViewer(); await this._loadViewer();
this._changeDetectorRef.markForCheck(); this._changeDetectorRef.markForCheck();
} }
private _loadViewer() { private async _loadViewer() {
if (!this._instance) { if (this._instance) {
WebViewer( return;
}
this._instance = await WebViewer(
{ {
licenseKey: this._licenseService.activeLicenseKey, licenseKey: this._licenseService.activeLicenseKey,
path: this._convertPath('/assets/wv-resources'), path: this._convertPath('/assets/wv-resources'),
@ -154,28 +157,23 @@ export class WatermarkScreenComponent {
isReadOnly: true, isReadOnly: true,
backendType: 'ems', backendType: 'ems',
}, },
this._viewer.nativeElement as HTMLElement, document.getElementById('viewer'),
).then(instance => { );
this._instance = instance;
instance.UI.setTheme(this._userPreferenceService.getTheme()); this._instance.UI.setTheme(this._userPreferenceService.getTheme());
instance.Core.documentViewer.on('documentLoaded', async () => { this._instance.Core.documentViewer.addEventListener('documentLoaded', async () => {
this._loadingService.stop(); this._loadingService.stop();
await this._drawWatermark(); await this._drawWatermark();
}); });
this._disableElements(); this._disableElements();
return this._http const request = this._http.get('/assets/pdftron/blank.pdf', {
.request('get', '/assets/pdftron/blank.pdf', {
responseType: 'blob', responseType: 'blob',
}) });
.subscribe(blobData => { const blobData = await firstValueFrom(request);
this._instance.UI.loadDocument(blobData, { filename: 'blank.pdf' }); this._instance.UI.loadDocument(blobData, { filename: 'blank.pdf' });
});
});
}
} }
private _disableElements() { private _disableElements() {
@ -186,22 +184,15 @@ export class WatermarkScreenComponent {
const pdfNet = this._instance.Core.PDFNet; const pdfNet = this._instance.Core.PDFNet;
const document = await this._instance.Core.documentViewer.getDocument().getPDFDoc(); const document = await this._instance.Core.documentViewer.getDocument().getPDFDoc();
const text: string = this.form.get('text').value || '';
const fontSize: number = this.form.get('fontSize').value;
const fontType: string = this.form.get('fontType').value;
const orientation: WatermarkOrientation = this.form.get('orientation').value;
const opacity: number = this.form.get('opacity').value;
const color: string = this.form.get('hexColor').value;
await stampPDFPage( await stampPDFPage(
document, document,
pdfNet, pdfNet,
text, this.form.controls.text.value || '',
fontSize, this.form.controls.fontSize.value,
fontType, this.form.controls.fontType.value,
orientation, this.form.controls.orientation.value,
opacity, this.form.controls.opacity.value,
color, this.form.controls.hexColor.value,
[1], [1],
this._licenseService.activeLicenseKey, this._licenseService.activeLicenseKey,
); );
@ -210,8 +201,8 @@ export class WatermarkScreenComponent {
this._changeDetectorRef.detectChanges(); this._changeDetectorRef.detectChanges();
} }
private _getForm(): UntypedFormGroup { private _getForm() {
const form = this._formBuilder.group({ const form: FormGroup<AsControl<WatermarkForm>> = this._formBuilder.group({
name: [null], name: [null],
text: [null], text: [null],
hexColor: [null], hexColor: [null],

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'; import { Component } from '@angular/core';
import { import {
CircleButtonTypes, CircleButtonTypes,
ConfirmationDialogInput, ConfirmationDialogInput,
@ -14,7 +14,6 @@ import {
} from '@iqser/common-ui'; } from '@iqser/common-ui';
import { DOSSIER_TEMPLATE_ID, User, Watermark } from '@red/domain'; import { DOSSIER_TEMPLATE_ID, User, Watermark } from '@red/domain';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { firstValueFrom } from 'rxjs';
import { WatermarkService } from '@services/entity-services/watermark.service'; import { WatermarkService } from '@services/entity-services/watermark.service';
import { AdminDialogService } from '../../../services/admin-dialog.service'; import { AdminDialogService } from '../../../services/admin-dialog.service';
import { WatermarksMapService } from '@services/entity-services/watermarks-map.service'; import { WatermarksMapService } from '@services/entity-services/watermarks-map.service';
@ -23,7 +22,6 @@ import { ROLES } from '@users/roles';
@Component({ @Component({
templateUrl: './watermarks-listing-screen.component.html', templateUrl: './watermarks-listing-screen.component.html',
styleUrls: ['./watermarks-listing-screen.component.scss'], styleUrls: ['./watermarks-listing-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: listingProvidersFactory(WatermarksListingScreenComponent), providers: listingProvidersFactory(WatermarksListingScreenComponent),
}) })
export class WatermarksListingScreenComponent extends ListingComponent<Watermark> { export class WatermarksListingScreenComponent extends ListingComponent<Watermark> {
@ -53,8 +51,8 @@ export class WatermarksListingScreenComponent extends ListingComponent<Watermark
this.entitiesService.setEntities(this._watermarksMapService.get(this.#dossierTemplateId)); this.entitiesService.setEntities(this._watermarksMapService.get(this.#dossierTemplateId));
} }
async openConfirmDeleteWatermarkDialog($event: MouseEvent, watermark: Watermark): Promise<void> { async openConfirmDeleteWatermarkDialog($event: MouseEvent, watermark: Watermark) {
const isUsed = await firstValueFrom(this._watermarkService.isWatermarkUsed(watermark.id)); const isUsed = await this._watermarkService.isWatermarkUsed(watermark.id);
const data = new ConfirmationDialogInput({ const data = new ConfirmationDialogInput({
question: isUsed ? _('watermarks-listing.watermark-is-used') : null, question: isUsed ? _('watermarks-listing.watermark-is-used') : null,
@ -66,7 +64,7 @@ export class WatermarksListingScreenComponent extends ListingComponent<Watermark
async toggleStatus(watermark: Watermark): Promise<void> { async toggleStatus(watermark: Watermark): Promise<void> {
this._loadingService.start(); this._loadingService.start();
const updatedWatermark = await firstValueFrom(this._watermarkService.saveWatermark({ ...watermark, enabled: !watermark.enabled })); const updatedWatermark = await this._watermarkService.saveWatermark({ ...watermark, enabled: !watermark.enabled });
this.entitiesService.replace(updatedWatermark); this.entitiesService.replace(updatedWatermark);
this._loadingService.stop(); this._loadingService.stop();
} }
@ -77,7 +75,7 @@ export class WatermarksListingScreenComponent extends ListingComponent<Watermark
private async _deleteWatermark(watermark: Watermark): Promise<void> { private async _deleteWatermark(watermark: Watermark): Promise<void> {
this._loadingService.start(); this._loadingService.start();
await firstValueFrom(this._watermarkService.deleteWatermark(this.#dossierTemplateId, watermark.id)); await this._watermarkService.deleteWatermark(this.#dossierTemplateId, watermark.id);
this.entitiesService.setEntities(this._watermarksMapService.get(this.#dossierTemplateId)); this.entitiesService.setEntities(this._watermarksMapService.get(this.#dossierTemplateId));
this._toaster.success(_('watermarks-listing.action.delete-success')); this._toaster.success(_('watermarks-listing.action.delete-success'));
this._loadingService.stop(); this._loadingService.stop();

View File

@ -1,8 +1,8 @@
import { Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { GenericService, mapEach, QueryParam, RequiredParam, Validate } from '@iqser/common-ui'; import { GenericService, mapEach, QueryParam, RequiredParam, Validate } from '@iqser/common-ui';
import { IWatermark, Watermark } from '@red/domain'; import { IWatermark, Watermark } from '@red/domain';
import { forkJoin, Observable } from 'rxjs'; import { firstValueFrom, forkJoin, Observable } from 'rxjs';
import { map, switchMap, tap } from 'rxjs/operators'; import { map, tap } from 'rxjs/operators';
import { WatermarksMapService } from '@services/entity-services/watermarks-map.service'; import { WatermarksMapService } from '@services/entity-services/watermarks-map.service';
interface IsUsedResponse { interface IsUsedResponse {
@ -14,27 +14,19 @@ interface IsUsedResponse {
}) })
export class WatermarkService extends GenericService<IWatermark> { export class WatermarkService extends GenericService<IWatermark> {
protected readonly _defaultModelPath = 'watermark'; protected readonly _defaultModelPath = 'watermark';
readonly #watermarksMapService = inject(WatermarksMapService);
constructor(private readonly _watermarksMapService: WatermarksMapService) { @Validate()
super(); async deleteWatermark(@RequiredParam() dossierTemplateId: string, @RequiredParam() watermarkId: number) {
await firstValueFrom(super.delete(null, `${this._defaultModelPath}/${watermarkId}`));
return firstValueFrom(this.loadForDossierTemplate(dossierTemplateId));
} }
@Validate() @Validate()
deleteWatermark(@RequiredParam() dossierTemplateId: string, @RequiredParam() watermarkId: number): Observable<Watermark[]> { async saveWatermark(@RequiredParam() body: IWatermark) {
return super const watermark = await firstValueFrom(this._post(body, `${this._defaultModelPath}`));
.delete(null, `${this._defaultModelPath}/${watermarkId}`) await firstValueFrom(this.loadForDossierTemplate(watermark.dossierTemplateId));
.pipe(switchMap(() => this.loadForDossierTemplate(dossierTemplateId))); return this.#watermarksMapService.get(watermark.dossierTemplateId, watermark.id);
}
@Validate()
saveWatermark(@RequiredParam() body: IWatermark): Observable<Watermark> {
return this._post(body, `${this._defaultModelPath}`).pipe(
switchMap(watermark =>
this.loadForDossierTemplate(watermark.dossierTemplateId).pipe(
map(() => this._watermarksMapService.get(watermark.dossierTemplateId, watermark.id)),
),
),
);
} }
@Validate() @Validate()
@ -42,7 +34,7 @@ export class WatermarkService extends GenericService<IWatermark> {
const queryParams: QueryParam[] = [{ key: 'dossierTemplateId', value: dossierTemplateId }]; const queryParams: QueryParam[] = [{ key: 'dossierTemplateId', value: dossierTemplateId }];
return this.getAll(this._defaultModelPath, queryParams).pipe( return this.getAll(this._defaultModelPath, queryParams).pipe(
mapEach(entity => new Watermark(entity)), mapEach(entity => new Watermark(entity)),
tap(entities => this._watermarksMapService.set(dossierTemplateId, entities.sort(this.sortByStatusFn))), tap(entities => this.#watermarksMapService.set(dossierTemplateId, entities.sort(this.sortByStatusFn))),
); );
} }
@ -52,9 +44,10 @@ export class WatermarkService extends GenericService<IWatermark> {
} }
@Validate() @Validate()
isWatermarkUsed(@RequiredParam() watermarkId: number): Observable<boolean> { async isWatermarkUsed(@RequiredParam() watermarkId: number) {
const queryParams: QueryParam[] = [{ key: 'watermarkId', value: watermarkId }]; const queryParams: QueryParam[] = [{ key: 'watermarkId', value: watermarkId }];
return this.getAll<IsUsedResponse>(`${this._defaultModelPath}/used`, queryParams).pipe(map(result => result.value)); const result = await firstValueFrom(this.getAll<IsUsedResponse>(`${this._defaultModelPath}/used`, queryParams));
return result.value;
} }
sortByStatusFn = (a: Watermark, b: Watermark) => { sortByStatusFn = (a: Watermark, b: Watermark) => {

View File

@ -1,6 +1,7 @@
import type { List } from '@iqser/common-ui'; import type { List } from '@iqser/common-ui';
import type { AnnotationWrapper } from '@models/file/annotation.wrapper'; import type { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { Dayjs } from 'dayjs'; import { Dayjs } from 'dayjs';
import { ITrackable } from '../../../../../libs/common-ui/src/lib/listing/models/trackable';
export function hexToRgb(hex: string) { export function hexToRgb(hex: string) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
@ -89,7 +90,10 @@ export function compareLists(l1: string[], l2: string[]) {
} }
export const byPage = (page: number) => (annotation: AnnotationWrapper) => annotation.pageNumber === page; export const byPage = (page: number) => (annotation: AnnotationWrapper) => annotation.pageNumber === page;
export const byId = (id: string) => (annotation: AnnotationWrapper) => annotation.annotationId === id;
export function byId<T extends ITrackable>(id: string) {
return (item: T) => item.id === id;
}
export function getLast<T>(list: List<T>) { export function getLast<T>(list: List<T>) {
return list[list.length - 1]; return list[list.length - 1];

View File

@ -12,16 +12,16 @@ async function createPageSet(pdfNet: typeof Core.PDFNet, pages: number[]) {
return pageSet; return pageSet;
} }
function convertFont(type: string): number { function convertFont(pdfNet: typeof Core.PDFNet, type: string): number {
switch (type) { switch (type) {
case 'times-new-roman': case 'times-new-roman':
return 0; return pdfNet.Font.StandardType1Font.e_times_roman;
case 'helvetica': case 'helvetica':
return 4; return pdfNet.Font.StandardType1Font.e_helvetica;
case 'courier': case 'courier':
return 8; return pdfNet.Font.StandardType1Font.e_courier;
} }
return 4; return pdfNet.Font.StandardType1Font.e_helvetica;
} }
export async function clearStamps(document: PDFDoc, pdfNet: typeof Core.PDFNet, pages: number[], licenseKey: string) { export async function clearStamps(document: PDFDoc, pdfNet: typeof Core.PDFNet, pages: number[], licenseKey: string) {
@ -34,7 +34,7 @@ export async function clearStamps(document: PDFDoc, pdfNet: typeof Core.PDFNet,
export async function stampPDFPage( export async function stampPDFPage(
document: PDFDoc, document: PDFDoc,
pdfNet: any, pdfNet: typeof Core.PDFNet,
text: string, text: string,
fontSize: number, fontSize: number,
fontType: string, fontType: string,
@ -50,10 +50,10 @@ export async function stampPDFPage(
const pageSet = await createPageSet(pdfNet, pages); const pageSet = await createPageSet(pdfNet, pages);
await pdfNet.Stamper.deleteStamps(document, pageSet); await pdfNet.Stamper.deleteStamps(document, pageSet);
const rgbColor = hexToRgb(color); const { b, g, r } = hexToRgb(color);
const stamper = await pdfNet.Stamper.create(3, fontSize, 0); const stamper = await pdfNet.Stamper.create(pdfNet.Stamper.SizeType.e_font_size, fontSize, 0);
await stamper.setFontColor(await pdfNet.ColorPt.init(rgbColor.r / 255, rgbColor.g / 255, rgbColor.b / 255)); await stamper.setFontColor(await pdfNet.ColorPt.init(r / 255, g / 255, b / 255));
await stamper.setOpacity(opacity / 100); await stamper.setOpacity(opacity / 100);
switch (orientation) { switch (orientation) {
@ -74,8 +74,10 @@ export async function stampPDFPage(
await stamper.setRotation(-45); await stamper.setRotation(-45);
} }
const font = await pdfNet.Font.createAndEmbed(document, convertFont(fontType)); const initialFont = await pdfNet.Font.create(document, convertFont(pdfNet, fontType));
await stamper.setFont(font); // in case there are japanese characters in the text, we add them to the font
const fontWithAllTextChars = await pdfNet.Font.createFromFontDescriptor(document, initialFont, text);
await stamper.setFont(fontWithAllTextChars);
await stamper.setTextAlignment(0); await stamper.setTextAlignment(0);
await stamper.stampText(document, text, pageSet); await stamper.stampText(document, text, pageSet);
}, licenseKey); }, licenseKey);