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 { 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 { BaseFormComponent, IqserPermissionsService } from '@iqser/common-ui';
import { AsControl, BaseFormComponent, IqserPermissionsService } from '@iqser/common-ui';
import { ROLES } from '@users/roles';
interface PreferencesForm {
@ -16,7 +16,6 @@ interface PreferencesForm {
[k: string]: any;
}
type AsControl<T> = { [K in keyof T]: FormControl<T[K]> };
type Screen = 'preferences' | 'warnings-preferences';
const Screens = {

View File

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

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Component } from '@angular/core';
import {
CircleButtonTypes,
ConfirmationDialogInput,
@ -14,7 +14,6 @@ import {
} from '@iqser/common-ui';
import { DOSSIER_TEMPLATE_ID, User, Watermark } from '@red/domain';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { firstValueFrom } from 'rxjs';
import { WatermarkService } from '@services/entity-services/watermark.service';
import { AdminDialogService } from '../../../services/admin-dialog.service';
import { WatermarksMapService } from '@services/entity-services/watermarks-map.service';
@ -23,7 +22,6 @@ import { ROLES } from '@users/roles';
@Component({
templateUrl: './watermarks-listing-screen.component.html',
styleUrls: ['./watermarks-listing-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: listingProvidersFactory(WatermarksListingScreenComponent),
})
export class WatermarksListingScreenComponent extends ListingComponent<Watermark> {
@ -53,8 +51,8 @@ export class WatermarksListingScreenComponent extends ListingComponent<Watermark
this.entitiesService.setEntities(this._watermarksMapService.get(this.#dossierTemplateId));
}
async openConfirmDeleteWatermarkDialog($event: MouseEvent, watermark: Watermark): Promise<void> {
const isUsed = await firstValueFrom(this._watermarkService.isWatermarkUsed(watermark.id));
async openConfirmDeleteWatermarkDialog($event: MouseEvent, watermark: Watermark) {
const isUsed = await this._watermarkService.isWatermarkUsed(watermark.id);
const data = new ConfirmationDialogInput({
question: isUsed ? _('watermarks-listing.watermark-is-used') : null,
@ -66,7 +64,7 @@ export class WatermarksListingScreenComponent extends ListingComponent<Watermark
async toggleStatus(watermark: Watermark): Promise<void> {
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._loadingService.stop();
}
@ -77,7 +75,7 @@ export class WatermarksListingScreenComponent extends ListingComponent<Watermark
private async _deleteWatermark(watermark: Watermark): Promise<void> {
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._toaster.success(_('watermarks-listing.action.delete-success'));
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 { IWatermark, Watermark } from '@red/domain';
import { forkJoin, Observable } from 'rxjs';
import { map, switchMap, tap } from 'rxjs/operators';
import { firstValueFrom, forkJoin, Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { WatermarksMapService } from '@services/entity-services/watermarks-map.service';
interface IsUsedResponse {
@ -14,27 +14,19 @@ interface IsUsedResponse {
})
export class WatermarkService extends GenericService<IWatermark> {
protected readonly _defaultModelPath = 'watermark';
readonly #watermarksMapService = inject(WatermarksMapService);
constructor(private readonly _watermarksMapService: WatermarksMapService) {
super();
@Validate()
async deleteWatermark(@RequiredParam() dossierTemplateId: string, @RequiredParam() watermarkId: number) {
await firstValueFrom(super.delete(null, `${this._defaultModelPath}/${watermarkId}`));
return firstValueFrom(this.loadForDossierTemplate(dossierTemplateId));
}
@Validate()
deleteWatermark(@RequiredParam() dossierTemplateId: string, @RequiredParam() watermarkId: number): Observable<Watermark[]> {
return super
.delete(null, `${this._defaultModelPath}/${watermarkId}`)
.pipe(switchMap(() => this.loadForDossierTemplate(dossierTemplateId)));
}
@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)),
),
),
);
async saveWatermark(@RequiredParam() body: IWatermark) {
const watermark = await firstValueFrom(this._post(body, `${this._defaultModelPath}`));
await firstValueFrom(this.loadForDossierTemplate(watermark.dossierTemplateId));
return this.#watermarksMapService.get(watermark.dossierTemplateId, watermark.id);
}
@Validate()
@ -42,7 +34,7 @@ export class WatermarkService extends GenericService<IWatermark> {
const queryParams: QueryParam[] = [{ key: 'dossierTemplateId', value: dossierTemplateId }];
return this.getAll(this._defaultModelPath, queryParams).pipe(
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()
isWatermarkUsed(@RequiredParam() watermarkId: number): Observable<boolean> {
async isWatermarkUsed(@RequiredParam() watermarkId: number) {
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) => {

View File

@ -1,6 +1,7 @@
import type { List } from '@iqser/common-ui';
import type { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { Dayjs } from 'dayjs';
import { ITrackable } from '../../../../../libs/common-ui/src/lib/listing/models/trackable';
export function hexToRgb(hex: string) {
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 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>) {
return list[list.length - 1];

View File

@ -12,16 +12,16 @@ async function createPageSet(pdfNet: typeof Core.PDFNet, pages: number[]) {
return pageSet;
}
function convertFont(type: string): number {
function convertFont(pdfNet: typeof Core.PDFNet, type: string): number {
switch (type) {
case 'times-new-roman':
return 0;
return pdfNet.Font.StandardType1Font.e_times_roman;
case 'helvetica':
return 4;
return pdfNet.Font.StandardType1Font.e_helvetica;
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) {
@ -34,7 +34,7 @@ export async function clearStamps(document: PDFDoc, pdfNet: typeof Core.PDFNet,
export async function stampPDFPage(
document: PDFDoc,
pdfNet: any,
pdfNet: typeof Core.PDFNet,
text: string,
fontSize: number,
fontType: string,
@ -50,10 +50,10 @@ export async function stampPDFPage(
const pageSet = await createPageSet(pdfNet, pages);
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);
await stamper.setFontColor(await pdfNet.ColorPt.init(rgbColor.r / 255, rgbColor.g / 255, rgbColor.b / 255));
const stamper = await pdfNet.Stamper.create(pdfNet.Stamper.SizeType.e_font_size, fontSize, 0);
await stamper.setFontColor(await pdfNet.ColorPt.init(r / 255, g / 255, b / 255));
await stamper.setOpacity(opacity / 100);
switch (orientation) {
@ -74,8 +74,10 @@ export async function stampPDFPage(
await stamper.setRotation(-45);
}
const font = await pdfNet.Font.createAndEmbed(document, convertFont(fontType));
await stamper.setFont(font);
const initialFont = await pdfNet.Font.create(document, convertFont(pdfNet, fontType));
// 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.stampText(document, text, pageSet);
}, licenseKey);