RoyLiu 5 лет назад
Родитель
Сommit
e672247a40

+ 34 - 90
components/Watermark/index.tsx

@@ -1,99 +1,43 @@
 import React from 'react';
 
-import Button from '../Button';
-import Icon from '../Icon';
-import Drawer from '../Drawer';
-import Typography from '../Typography';
-import Tabs from '../Tabs';
-import Sliders from '../Sliders';
-import Divider from '../Divider';
-import ColorSelect from '../ColorSelector';
-import TextField from '../TextField';
+import { WatermarkType } from '../../constants/type';
 
-import { BtnWrapper, ContentWrapper } from './styled';
-import {
-  Container, Head, Body, Footer, IconWrapper,
-} from '../../global/sidebarStyled';
-import { Group, SliderWrapper } from '../../global/toolStyled';
+import { TextWrapper, Img } from './styled';
 
-type Props = {
-  onClick: () => void;
-  isActive: boolean;
+type Props = WatermarkType & {
+  viewScale: number;
 };
 
-const TextContent = (): React.ReactElement => (
-  <>
-    <ContentWrapper>
-      <Typography variant="subtitle">Content</Typography>
-    </ContentWrapper>
-    <TextField variant="multiline" shouldFitContainer />
-  </>
-);
-
-const ImageContent = (): React.ReactElement => (
-  <ContentWrapper>
-    <Icon glyph="add-image" />
-    <Button appearance="link" style={{ marginLeft: '10px' }}>Select Image</Button>
-  </ContentWrapper>
-);
-
-const Watermark: React.FunctionComponent<Props> = ({
-  onClick,
-  isActive,
+const index: React.FC<Props> = ({
+  type,
+  text,
+  imagepath,
+  viewScale,
+  scale = 1,
+  opacity,
+  textcolor,
+  rotation,
 }: Props) => (
-  <>
-    <BtnWrapper>
-      <Button shouldFitContainer align="left" onClick={onClick}>
-        <Icon glyph="watermark" style={{ marginRight: '10px' }} />
-        Watermark
-      </Button>
-    </BtnWrapper>
-    <Drawer open={isActive} anchor="left" zIndex={4}>
-      <Container>
-        <Head>
-          <IconWrapper>
-            <Icon glyph="left-back" onClick={onClick} />
-          </IconWrapper>
-          <Typography light>Watermark</Typography>
-        </Head>
-        <Body>
-          <Typography variant="subtitle">Type</Typography>
-          <Tabs
-            options={[
-              {
-                key: 'text',
-                content: 'Text',
-                child: <TextContent />,
-              },
-              {
-                key: 'image',
-                content: 'Image',
-                child: <ImageContent />,
-              },
-            ]}
-          />
-          <Divider orientation="horizontal" style={{ margin: '20px 0' }} />
-          <ColorSelect showTitle color="" onClick={(): void => {}} />
-          <Typography variant="subtitle">Opacity</Typography>
-          <Group>
-            <SliderWrapper>
-              <Sliders />
-            </SliderWrapper>
-            40%
-          </Group>
-          <Divider orientation="horizontal" style={{ margin: '20px 0' }} />
-          <Group>
-            <Typography variant="subtitle">Page Range</Typography>
-            <Button appearance="primary-hollow" style={{ width: '50%' }}>All Pages</Button>
-          </Group>
-        </Body>
-        <Footer>
-          <Button appearance="primary">Save</Button>
-          <Button appearance="danger-link">Remove watermark</Button>
-        </Footer>
-      </Container>
-    </Drawer>
-  </>
+  type === 'text' ? (
+    <TextWrapper
+      style={{
+        fontSize: `${viewScale * scale * 24}px`,
+        opacity: `${opacity}%`,
+        color: textcolor,
+        transform: `rotate(-${rotation}deg)`,
+      }}
+    >
+      {text}
+    </TextWrapper>
+  ) : (
+    <Img
+      style={{
+        opacity: `${opacity}%`,
+        transform: `scale(${viewScale * scale}) rotate(-${rotation}deg)`,
+      }}
+      src={imagepath}
+    />
+  )
 );
 
-export default Watermark;
+export default index;

+ 2 - 6
components/Watermark/styled.ts

@@ -1,10 +1,6 @@
 import styled from 'styled-components';
 
-export const BtnWrapper = styled.div`
-  padding: 8px;
+export const TextWrapper = styled.span`
 `;
 
-export const ContentWrapper = styled.div`
-  margin-top: 20px;
-  display: flex;
-`;
+export const Img = styled.img``;

+ 39 - 0
components/WatermarkImageSelector/index.tsx

@@ -0,0 +1,39 @@
+import React from 'react';
+
+import Icon from '../Icon';
+import Button from '../Button';
+import { uploadFile } from '../../helpers/utility';
+
+import { ContentWrapper } from './styled';
+
+type Props = {
+  t: (key: string) => string;
+  onChange: (data: string) => void;
+};
+
+const ImageSelector: React.FC<Props> = ({
+  t,
+  onChange,
+}: Props): React.ReactElement => {
+  const handleClick = () => {
+    uploadFile('.png,.jpg,.jpeg,.svg')
+      .then((urlData) => {
+        onChange(urlData);
+      });
+  };
+
+  return (
+    <ContentWrapper>
+      <Icon glyph="add-image" />
+      <Button
+        appearance="link"
+        style={{ marginLeft: '10px' }}
+        onClick={handleClick}
+      >
+        {t('selectImage')}
+      </Button>
+    </ContentWrapper>
+  )
+};
+
+export default ImageSelector;

+ 6 - 0
components/WatermarkImageSelector/styled.ts

@@ -0,0 +1,6 @@
+import styled from 'styled-components';
+
+export const ContentWrapper = styled.div`
+  margin-top: 20px;
+  display: flex;
+`;

+ 148 - 0
components/WatermarkOption/index.tsx

@@ -0,0 +1,148 @@
+import React from 'react';
+import { WithTranslation } from 'next-i18next';
+import { withTranslation } from '../../i18n';
+
+import { WatermarkType, SelectOptionType } from '../../constants/type';
+import Button from '../Button';
+import Box from '../Box';
+import Icon from '../Icon';
+import Typography from '../Typography';
+import Tabs from '../Tabs';
+import Divider from '../Divider';
+import ColorSelect from '../ColorSelector';
+import SliderWithTitle from '../SliderWithTitle';
+import TextBox from '../WatermarkTextBox';
+import ImageSelector from '../WatermarkImageSelector';
+import PageSelector from '../WatermarkPageSelector';
+
+import {
+  Container, Head, Body, Footer, IconWrapper,
+} from '../../global/sidebarStyled';
+
+type i18nProps = {
+  t: (key: string) => string;
+};
+
+type OwnerProps = WatermarkType & {
+  onClick: () => void;
+  isActive: boolean;
+  onSave: () => void;
+  onDelete: () => void;
+  setDataState: (arg: Record<string, any>) => void;
+};
+
+type Props = i18nProps & OwnerProps;
+
+const WatermarkOption: React.FC<Props> = ({
+  t,
+  onClick,
+  type,
+  opacity,
+  scale = 1,
+  rotation,
+  textcolor,
+  text,
+  onSave,
+  onDelete,
+  setDataState = (): void => {
+    // do nothing
+  },
+}: Props) => (
+  <Container>
+    <Head>
+      <IconWrapper>
+        <Icon glyph="left-back" onClick={onClick} />
+      </IconWrapper>
+      <Typography light>
+        {t('watermark')}
+      </Typography>
+    </Head>
+    <Body>
+      <Typography variant="subtitle" align="left">
+        {t('type')}
+      </Typography>
+      <Tabs
+        options={[
+          {
+            key: 'text',
+            content: t('text'),
+            child: (
+              <TextBox
+                t={t}
+                defaultValue={text}
+                onChange={(value: string): void => { setDataState({ text: value }); }}
+              />
+            ),
+          },
+          {
+            key: 'image',
+            content: t('image'),
+            child: (
+              <ImageSelector
+                t={t}
+                onChange={(data: string): void => { setDataState({ imagepath: data }); }}
+              />
+            ),
+          },
+        ]}
+        onChange={(option: SelectOptionType): void => { setDataState({ type: option.key }); }}
+      />
+      {
+        type === 'text' ? (
+          <>
+            <Divider orientation="horizontal" style={{ margin: '20px 0' }} />
+            <ColorSelect
+              title={t('color')}
+              mode="watermark"
+              selectedColor={textcolor}
+              onClick={(val: string): void => { setDataState({ textcolor: val }); }}
+            />
+          </>
+        ) : null
+      }
+      <Box mt="20">
+        <SliderWithTitle
+          title={t('opacity')}
+          value={opacity}
+          tips={`${opacity}%`}
+          onSlide={(val: number): void => { setDataState({ opacity: val }); }}
+        />
+      </Box>
+      <Box mt="20">
+        <SliderWithTitle
+          title={t('rotate')}
+          value={rotation}
+          maximum={360}
+          tips={`${rotation}°`}
+          onSlide={(val: number): void => { setDataState({ rotation: val }); }}
+        />
+      </Box>
+      <Box mt="20">
+        <SliderWithTitle
+          title={t('scale')}
+          value={scale * 100}
+          tips={`${Math.round(scale * 100)}%`}
+          minimum={50}
+          maximum={250}
+          onSlide={(val: number): void => { setDataState({ scale: val / 100 }); }}
+        />
+      </Box>
+      <Divider orientation="horizontal" style={{ margin: '20px 0' }} />
+      <PageSelector t={t} />
+    </Body>
+    <Footer>
+      <Button appearance="primary" onClick={onSave}>
+        {t('save')}
+      </Button>
+      <Button appearance="danger-link" onClick={onDelete}>
+        {t('removeWatermark')}
+      </Button>
+    </Footer>
+  </Container>
+);
+
+const translator = withTranslation('sidebar');
+
+type TransProps = WithTranslation & OwnerProps;
+
+export default translator<TransProps>(WatermarkOption);

+ 5 - 0
components/WatermarkOption/styled.ts

@@ -0,0 +1,5 @@
+import styled from 'styled-components';
+
+export const BtnWrapper = styled.div`
+  padding: 8px;
+`;

+ 25 - 0
components/WatermarkPageSelector/index.tsx

@@ -0,0 +1,25 @@
+import React from 'react';
+
+import Button from '../Button';
+import Typography from '../Typography';
+
+import { Group } from '../../global/toolStyled';
+
+type Props = {
+  t: (key: string) => string;
+};
+
+const PageSelector: React.FC<Props> = ({
+  t,
+}: Props) => (
+  <Group>
+    <Typography variant="subtitle">
+      {t('pageRange')}
+    </Typography>
+    <Button appearance="primary-hollow" style={{ width: '50%' }}>
+      {t('All Pages')}
+    </Button>
+  </Group>
+);
+
+export default PageSelector;

+ 32 - 0
components/WatermarkTextBox/index.tsx

@@ -0,0 +1,32 @@
+import React from 'react';
+
+import TextField from '../TextField';
+import Typography from '../Typography';
+
+import { ContentWrapper } from './styled';
+
+type Props = {
+  t: (key: string) => string;
+  onChange: (value: string) => void;
+  defaultValue?: string;
+};
+
+const TextBox: React.FC<Props> = ({
+  t,
+  onChange,
+  defaultValue = '',
+}: Props): React.ReactElement => (
+  <>
+    <ContentWrapper>
+      <Typography variant="subtitle">{t('text')}</Typography>
+    </ContentWrapper>
+    <TextField
+      variant="multiline"
+      onChange={onChange}
+      defaultValue={defaultValue}
+      shouldFitContainer
+    />
+  </>
+);
+
+export default TextBox;

+ 6 - 0
components/WatermarkTextBox/styled.ts

@@ -0,0 +1,6 @@
+import styled from 'styled-components';
+
+export const ContentWrapper = styled.div`
+  margin-top: 20px;
+  display: flex;
+`;

+ 0 - 26
containers/Watermark.tsx

@@ -1,26 +0,0 @@
-import React, { useState } from 'react';
-
-import WatermarkComponent from '../components/Watermark';
-
-import useActions from '../actions';
-import useStore from '../store';
-
-const Watermark: React.FunctionComponent = () => {
-  const [isActive, setActive] = useState(false);
-  const [, dispatch] = useStore();
-  const { setSidebar } = useActions(dispatch);
-
-  const handleClick = (): void => {
-    setActive(!isActive);
-    setSidebar(!isActive ? 'watermark' : '');
-  };
-
-  return (
-    <WatermarkComponent
-      isActive={isActive}
-      onClick={handleClick}
-    />
-  );
-};
-
-export default Watermark;

+ 112 - 0
containers/WatermarkTool.tsx

@@ -0,0 +1,112 @@
+import React, { useState, useEffect } from 'react';
+import queryString from 'query-string';
+import { withTranslation } from '../i18n';
+
+import { ANNOTATION_TYPE } from '../constants';
+import { WatermarkType, AnnotationType } from '../constants/type';
+import Icon from '../components/Icon';
+import Button from '../components/Button';
+import Drawer from '../components/Drawer';
+import WatermarkOption from '../components/WatermarkOption';
+
+import useActions from '../actions';
+import useStore from '../store';
+
+import { BtnWrapper } from '../global/toolStyled';
+
+type Props = {
+  t: (key: string) => string;
+};
+
+const WatermarkTool: React.FC<Props> = ({
+  t,
+}: Props) => {
+  const [isActive, setActive] = useState(false);
+  const [{
+    totalPage,
+    watermark,
+    annotations,
+  }, dispatch] = useStore();
+  const {
+    setSidebar,
+    updateWatermark,
+    addAnnots,
+    updateAnnots,
+  } = useActions(dispatch);
+
+  const setDataState = (obj: WatermarkType): void => {
+    updateWatermark(obj);
+  };
+
+  const removeWatermarkInAnnots = (): void => {
+    const index = annotations.find((ele: AnnotationType) => ele.obj_type === 'watermark');
+    annotations.splice(index, 1);
+    updateAnnots(annotations);
+  };
+
+  const handleClick = (): void => {
+    setActive(!isActive);
+    setSidebar(!isActive ? 'watermark' : '');
+  };
+
+  const handleSave = (): void => {
+    const { type, imagepath, ...rest } = watermark;
+    const watermarkData = {
+      obj_type: ANNOTATION_TYPE.watermark,
+      obj_attr: {
+        ...rest,
+        type,
+        pages: `0-${totalPage}`,
+        opacity: (watermark.opacity || 0) * 0.01,
+        original_image_name: imagepath ? `temp.${imagepath.match(/image\/(.*);base64/)[1]}` : '',
+        image_data: type === 'image' ? imagepath.match(/base64,(.*)/)[1] : '',
+      },
+    };
+    removeWatermarkInAnnots();
+    addAnnots([watermarkData], true);
+  };
+
+  const handleDelete = (): void => {
+    removeWatermarkInAnnots();
+    updateWatermark({
+      ...watermark,
+      text: '',
+      imagepath: '',
+    });
+  };
+
+  useEffect(() => {
+    const parsed = queryString.parse(window.location.search);
+
+    if (parsed.watermark) {
+      setDataState({
+        text: parsed.watermark as string,
+      });
+    }
+  }, []);
+
+  return (
+    <>
+      <BtnWrapper>
+        <Button shouldFitContainer align="left" onClick={handleClick}>
+          <Icon glyph="watermark" style={{ marginRight: '10px' }} />
+          {t('watermark')}
+        </Button>
+      </BtnWrapper>
+      <Drawer open={isActive} anchor="left" zIndex={4}>
+        <WatermarkOption
+          isActive={isActive}
+          onClick={handleClick}
+          onSave={handleSave}
+          onDelete={handleDelete}
+          setDataState={setDataState}
+          {...watermark}
+        />
+      </Drawer>
+    </>
+  );
+};
+
+const translator = withTranslation('sidebar');
+
+export default translator(WatermarkTool);

+ 48 - 0
helpers/watermark.ts

@@ -0,0 +1,48 @@
+import {
+  WatermarkType,
+} from '../constants/type';
+import { floatToHex } from './utility';
+import { xmlParser, getElementsByTagname } from './dom';
+
+export const parseWatermarkFromXml = (xmlString: string): WatermarkType => {
+  if (!xmlString) return {};
+  const xmlDoc = xmlParser(xmlString);
+  const elements = xmlDoc.firstElementChild || xmlDoc.firstChild;
+  const data: WatermarkType = {};
+  const element = getElementsByTagname(elements, 'watermarks');
+  if (element[1]) {
+    const array: any[] = Array.prototype.slice.call(element[1].childNodes);
+    array.forEach((ele, index) => {
+      switch (ele.tagName) {
+        case 'Font':
+          data.type = 'text';
+          data.text = array[index + 1].textContent.trim();
+          break;
+        case 'Opacity': {
+          data.opacity = ele.attributes.value.value * 100;
+          break;
+        }
+        case 'Rotation': {
+          data.rotation = parseFloat(ele.attributes.value.value);
+          break;
+        }
+        case 'Scale': {
+          data.scale = parseFloat(ele.attributes.value.value);
+          break;
+        }
+        case 'Color': {
+          const r = ele.attributes.r.value;
+          const g = ele.attributes.g.value;
+          const b = ele.attributes.b.value;
+          data.textcolor = floatToHex(r, g, b);
+          break;
+        }
+        default:
+          break;
+      }
+    });
+  }
+  return data;
+};
+
+export default parseWatermarkFromXml;

+ 22 - 1
store/initialPdfState.ts

@@ -1,4 +1,10 @@
-import { ProgressType, ViewportType, AnnotationType } from '../constants/type';
+import {
+  ProgressType,
+  ViewportType,
+  AnnotationType,
+  WatermarkType,
+} from '../constants/type';
+import { color as theme } from '../constants/style';
 
 export type StateType = {
   totalPage: number;
@@ -10,6 +16,7 @@ export type StateType = {
   rotation: number;
   annotations: AnnotationType[];
   isInit: boolean;
+  watermark: WatermarkType;
 };
 
 export default {
@@ -25,4 +32,18 @@ export default {
   rotation: 0,
   annotations: [],
   isInit: false,
+  watermark: {
+    type: 'text',
+    textcolor: theme.gray,
+    opacity: 50,
+    text: '',
+    scale: 1,
+    rotation: 0,
+    vertalign: 'center',
+    horizalign: 'center',
+    xoffset: 0,
+    yoffset: 0,
+    imagepath: '',
+    isfront: 'yes',
+  },
 };