filter at pdf generation and review screen (#11)
* filter at pdf generation * full filter support via external package * added to jest ignore * fix intry * fix filter size * brightness weirdness fix * fix preview screen colors
This commit is contained in:
4
App.tsx
4
App.tsx
@@ -29,8 +29,8 @@ export default function App() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSaveDocument = useCallback(
|
const handleSaveDocument = useCallback(
|
||||||
(pages: ScannedPage[], _filter: FilterMode) => {
|
(pages: ScannedPage[], filter: FilterMode) => {
|
||||||
const doc = createDocument(pages);
|
const doc = createDocument(pages, filter);
|
||||||
setPendingPages([]);
|
setPendingPages([]);
|
||||||
setViewingDoc(doc);
|
setViewingDoc(doc);
|
||||||
setScreen('viewer');
|
setScreen('viewer');
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
package com.lensapp
|
package com.lensapp
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.ColorMatrix
|
||||||
|
import android.graphics.ColorMatrixColorFilter
|
||||||
|
import android.graphics.Paint
|
||||||
import android.graphics.RectF
|
import android.graphics.RectF
|
||||||
import android.graphics.pdf.PdfDocument
|
import android.graphics.pdf.PdfDocument
|
||||||
import com.facebook.react.bridge.Promise
|
import com.facebook.react.bridge.Promise
|
||||||
@@ -16,8 +21,15 @@ class PdfGeneratorModule(private val reactContext: ReactApplicationContext) :
|
|||||||
|
|
||||||
override fun getName() = "PdfGenerator"
|
override fun getName() = "PdfGenerator"
|
||||||
|
|
||||||
|
private fun applyFilter(src: Bitmap, matrixValues: ReadableArray): Bitmap {
|
||||||
|
val floats = FloatArray(20) { matrixValues.getDouble(it).toFloat() }
|
||||||
|
val out = Bitmap.createBitmap(src.width, src.height, Bitmap.Config.ARGB_8888)
|
||||||
|
Canvas(out).drawBitmap(src, 0f, 0f, Paint().apply { colorFilter = ColorMatrixColorFilter(ColorMatrix(floats)) })
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
fun generate(imagePaths: ReadableArray, fileName: String, promise: Promise) {
|
fun generate(imagePaths: ReadableArray, fileName: String, matrix: ReadableArray, promise: Promise) {
|
||||||
try {
|
try {
|
||||||
val outDir = File(reactContext.filesDir, "pdfs").also { it.mkdirs() }
|
val outDir = File(reactContext.filesDir, "pdfs").also { it.mkdirs() }
|
||||||
val outFile = File(outDir, "$fileName.pdf")
|
val outFile = File(outDir, "$fileName.pdf")
|
||||||
@@ -25,8 +37,10 @@ class PdfGeneratorModule(private val reactContext: ReactApplicationContext) :
|
|||||||
|
|
||||||
for (i in 0 until imagePaths.size()) {
|
for (i in 0 until imagePaths.size()) {
|
||||||
val path = imagePaths.getString(i)!!.removePrefix("file://")
|
val path = imagePaths.getString(i)!!.removePrefix("file://")
|
||||||
val bitmap = BitmapFactory.decodeFile(path)
|
val raw = BitmapFactory.decodeFile(path)
|
||||||
?: throw Exception("Failed to decode image: $path")
|
?: throw Exception("Failed to decode image: $path")
|
||||||
|
val bitmap = applyFilter(raw, matrix)
|
||||||
|
if (raw !== bitmap) raw.recycle()
|
||||||
|
|
||||||
// A4 at 72 DPI: 595 x 842 points
|
// A4 at 72 DPI: 595 x 842 points
|
||||||
val pageWidth = 595
|
val pageWidth = 595
|
||||||
|
|||||||
@@ -1,12 +1,29 @@
|
|||||||
#import "PdfGenerator.h"
|
#import "PdfGenerator.h"
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
|
#import <CoreImage/CoreImage.h>
|
||||||
|
|
||||||
@implementation PdfGenerator
|
@implementation PdfGenerator
|
||||||
|
|
||||||
RCT_EXPORT_MODULE();
|
RCT_EXPORT_MODULE();
|
||||||
|
|
||||||
|
- (UIImage *)applyMatrix:(UIImage *)image matrix:(NSArray *)m {
|
||||||
|
CIImage *ci = [CIImage imageWithCGImage:image.CGImage];
|
||||||
|
CIFilter *f = [CIFilter filterWithName:@"CIColorMatrix"];
|
||||||
|
[f setValue:ci forKey:kCIInputImageKey];
|
||||||
|
[f setValue:[CIVector vectorWithX:[m[0] floatValue] Y:[m[1] floatValue] Z:[m[2] floatValue] W:[m[3] floatValue]] forKey:@"inputRVector"];
|
||||||
|
[f setValue:[CIVector vectorWithX:[m[5] floatValue] Y:[m[6] floatValue] Z:[m[7] floatValue] W:[m[8] floatValue]] forKey:@"inputGVector"];
|
||||||
|
[f setValue:[CIVector vectorWithX:[m[10] floatValue] Y:[m[11] floatValue] Z:[m[12] floatValue] W:[m[13] floatValue]] forKey:@"inputBVector"];
|
||||||
|
[f setValue:[CIVector vectorWithX:[m[15] floatValue] Y:[m[16] floatValue] Z:[m[17] floatValue] W:[m[18] floatValue]] forKey:@"inputAVector"];
|
||||||
|
[f setValue:[CIVector vectorWithX:[m[4] floatValue] Y:[m[9] floatValue] Z:[m[14] floatValue] W:[m[19] floatValue]] forKey:@"inputBiasVector"];
|
||||||
|
CGImageRef cg = [[CIContext context] createCGImage:f.outputImage fromRect:f.outputImage.extent];
|
||||||
|
UIImage *result = [UIImage imageWithCGImage:cg];
|
||||||
|
CGImageRelease(cg);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
RCT_EXPORT_METHOD(generate:(NSArray *)imagePaths
|
RCT_EXPORT_METHOD(generate:(NSArray *)imagePaths
|
||||||
fileName:(NSString *)fileName
|
fileName:(NSString *)fileName
|
||||||
|
matrix:(NSArray *)matrix
|
||||||
resolver:(RCTPromiseResolveBlock)resolve
|
resolver:(RCTPromiseResolveBlock)resolve
|
||||||
rejecter:(RCTPromiseRejectBlock)reject)
|
rejecter:(RCTPromiseRejectBlock)reject)
|
||||||
{
|
{
|
||||||
@@ -31,8 +48,9 @@ RCT_EXPORT_METHOD(generate:(NSArray *)imagePaths
|
|||||||
withActions:^(UIGraphicsPDFRendererContext *ctx) {
|
withActions:^(UIGraphicsPDFRendererContext *ctx) {
|
||||||
for (NSString *rawPath in imagePaths) {
|
for (NSString *rawPath in imagePaths) {
|
||||||
NSString *path = [rawPath hasPrefix:@"file://"] ? [rawPath substringFromIndex:7] : rawPath;
|
NSString *path = [rawPath hasPrefix:@"file://"] ? [rawPath substringFromIndex:7] : rawPath;
|
||||||
UIImage *image = [UIImage imageWithContentsOfFile:path];
|
UIImage *raw = [UIImage imageWithContentsOfFile:path];
|
||||||
if (!image) continue;
|
if (!raw) continue;
|
||||||
|
UIImage *image = [self applyMatrix:raw matrix:matrix];
|
||||||
|
|
||||||
[ctx beginPage];
|
[ctx beginPage];
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
|
||||||
transformIgnorePatterns: [
|
transformIgnorePatterns: [
|
||||||
'node_modules/(?!(react-native|@react-native|react-native-document-scanner-plugin|react-native-fs|react-native-html-to-pdf|react-native-share)/)',
|
'node_modules/(?!(react-native|@react-native|react-native-document-scanner-plugin|react-native-fs|react-native-html-to-pdf|react-native-share|react-native-color-matrix-image-filters|rn-color-matrices|concat-color-matrices)/)',
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
49
package-lock.json
generated
49
package-lock.json
generated
@@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-native": "0.73.0",
|
"react-native": "0.73.0",
|
||||||
|
"react-native-color-matrix-image-filters": "^8.0.2",
|
||||||
"react-native-document-scanner-plugin": "^2.0.4",
|
"react-native-document-scanner-plugin": "^2.0.4",
|
||||||
"react-native-fs": "^2.20.0",
|
"react-native-fs": "^2.20.0",
|
||||||
"react-native-share": "^12.2.6"
|
"react-native-share": "^12.2.6"
|
||||||
@@ -5840,6 +5841,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/clamp": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/clamp/-/clamp-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-kgMuFyE78OC6Dyu3Dy7vcx4uy97EIbVxJB/B0eJ3bUNAkwdNcxYzgKltnyADiYwsR7SEqkkUPsEUT//OVS6XMA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cli-cursor": {
|
"node_modules/cli-cursor": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||||
@@ -6018,6 +6025,15 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/concat-color-matrices": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/concat-color-matrices/-/concat-color-matrices-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-K0IMtl4m9LcHg4vVFb40Txmie/GwCJBkquGSn6wtA9yIcszzFZgGc6co4sSnjFdqeJAv+q2LrCHMUSb2GHAChA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"invariant": "^2.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -12385,6 +12401,21 @@
|
|||||||
"react": "18.2.0"
|
"react": "18.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-color-matrix-image-filters": {
|
||||||
|
"version": "8.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-color-matrix-image-filters/-/react-native-color-matrix-image-filters-8.0.2.tgz",
|
||||||
|
"integrity": "sha512-RJoNJt5Mhi1ztsPdKB3kNDHMSbarrWdNkcY2gPb3G7N9Kx6TKq00hl/X90PWRk9p2lqgiD0YznD8NlxDgqgLPg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"concat-color-matrices": "^1.0.0",
|
||||||
|
"rn-color-matrices": "^4.1.0",
|
||||||
|
"ts-tiny-invariant": "^2.0.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-document-scanner-plugin": {
|
"node_modules/react-native-document-scanner-plugin": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-document-scanner-plugin/-/react-native-document-scanner-plugin-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-document-scanner-plugin/-/react-native-document-scanner-plugin-2.0.4.tgz",
|
||||||
@@ -12789,6 +12820,18 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rn-color-matrices": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/rn-color-matrices/-/rn-color-matrices-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-a8++z3Zi4GhA0oWwKS3etdrVuVQ2q/ByTMh6lMxo+vaSv3SRSSg5SzpZk65GS0NCAqMhFT8WSvgSgNhO/F+xag==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"clamp": "^1.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/run-parallel": {
|
"node_modules/run-parallel": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||||
@@ -13853,6 +13896,12 @@
|
|||||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/ts-tiny-invariant": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-tiny-invariant/-/ts-tiny-invariant-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-NGQzWRLGLMjOUTpsxMSRj63fuFBC8HV8L4NUxzDVgU4MFeOt3qFI2534M5L+ch22x53oDEwPaRBPXgAfRjcXHA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-native": "0.73.0",
|
"react-native": "0.73.0",
|
||||||
|
"react-native-color-matrix-image-filters": "^8.0.2",
|
||||||
"react-native-document-scanner-plugin": "^2.0.4",
|
"react-native-document-scanner-plugin": "^2.0.4",
|
||||||
"react-native-fs": "^2.20.0",
|
"react-native-fs": "^2.20.0",
|
||||||
"react-native-share": "^12.2.6"
|
"react-native-share": "^12.2.6"
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ jest.mock('react-native', () => ({
|
|||||||
generate: jest.fn(),
|
generate: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Platform: {OS: 'android'},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../utils/filterMatrices', () => ({
|
||||||
|
getFilterMatrix: jest.fn(() => new Array(20).fill(0)),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import {NativeModules} from 'react-native';
|
import {NativeModules} from 'react-native';
|
||||||
@@ -30,6 +35,7 @@ describe('generatePdf', () => {
|
|||||||
expect(mockGenerate).toHaveBeenCalledWith(
|
expect(mockGenerate).toHaveBeenCalledWith(
|
||||||
['file:///mock/scans/scan1.jpg', '/mock/scans/scan2.jpg'],
|
['file:///mock/scans/scan1.jpg', '/mock/scans/scan2.jpg'],
|
||||||
'test',
|
'test',
|
||||||
|
expect.any(Array),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {View, Text, Image, TouchableOpacity, StyleSheet} from 'react-native';
|
import {View, Text, Image, TouchableOpacity, StyleSheet} from 'react-native';
|
||||||
|
import {ColorMatrix} from 'react-native-color-matrix-image-filters';
|
||||||
import {ScannedDocument} from '../types';
|
import {ScannedDocument} from '../types';
|
||||||
import {formatDate} from '../utils/imageProcessing';
|
import {formatDate} from '../utils/imageProcessing';
|
||||||
|
import {getFilterMatrix} from '../utils/filterMatrices';
|
||||||
import {useTheme} from '../theme';
|
import {useTheme} from '../theme';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -20,7 +22,9 @@ export default function DocumentCard({document, onPress}: Props) {
|
|||||||
activeOpacity={0.8}>
|
activeOpacity={0.8}>
|
||||||
<View style={styles.thumbContainer}>
|
<View style={styles.thumbContainer}>
|
||||||
{thumb ? (
|
{thumb ? (
|
||||||
|
<ColorMatrix matrix={getFilterMatrix(document.filter)} style={styles.thumb}>
|
||||||
<Image source={{uri: thumb}} style={styles.thumb} resizeMode="cover" />
|
<Image source={{uri: thumb}} style={styles.thumb} resizeMode="cover" />
|
||||||
|
</ColorMatrix>
|
||||||
) : (
|
) : (
|
||||||
<View style={[styles.thumb, {backgroundColor: t.border}]} />
|
<View style={[styles.thumb, {backgroundColor: t.border}]} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {useState, useCallback, useEffect, useRef} from 'react';
|
import {useState, useCallback, useEffect, useRef} from 'react';
|
||||||
import RNFS from 'react-native-fs';
|
import RNFS from 'react-native-fs';
|
||||||
import {ScannedDocument, ScannedPage} from '../types';
|
import {ScannedDocument, ScannedPage, FilterMode} from '../types';
|
||||||
|
|
||||||
let idCounter = 0;
|
let idCounter = 0;
|
||||||
const uid = () => `${Date.now()}-${++idCounter}`;
|
const uid = () => `${Date.now()}-${++idCounter}`;
|
||||||
@@ -42,12 +42,13 @@ export function useDocumentStore() {
|
|||||||
}
|
}
|
||||||
}, [documents]);
|
}, [documents]);
|
||||||
|
|
||||||
const createDocument = useCallback((pages: ScannedPage[]): ScannedDocument => {
|
const createDocument = useCallback((pages: ScannedPage[], filter: FilterMode): ScannedDocument => {
|
||||||
const doc: ScannedDocument = {
|
const doc: ScannedDocument = {
|
||||||
id: uid(),
|
id: uid(),
|
||||||
pages,
|
pages,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
name: `Scan ${new Date().toLocaleDateString()}`,
|
name: `Scan ${new Date().toLocaleDateString()}`,
|
||||||
|
filter,
|
||||||
};
|
};
|
||||||
setDocuments(prev => [doc, ...prev]);
|
setDocuments(prev => [doc, ...prev]);
|
||||||
return doc;
|
return doc;
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
StatusBar,
|
StatusBar,
|
||||||
Alert,
|
Alert,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import {ColorMatrix} from 'react-native-color-matrix-image-filters';
|
||||||
|
import {getFilterMatrix} from '../utils/filterMatrices';
|
||||||
import {ScannedPage, FilterMode} from '../types';
|
import {ScannedPage, FilterMode} from '../types';
|
||||||
import FilterPicker from '../components/FilterPicker';
|
import FilterPicker from '../components/FilterPicker';
|
||||||
import {useTheme} from '../theme';
|
import {useTheme} from '../theme';
|
||||||
@@ -28,14 +30,11 @@ export default function ReviewScreen({pages, onSave, onAddMore, onCancel}: Props
|
|||||||
const [filter, setFilter] = useState<FilterMode>('original');
|
const [filter, setFilter] = useState<FilterMode>('original');
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
|
|
||||||
const imageContainerStyle = () => {
|
const renderImage = (page: ScannedPage) => (
|
||||||
switch (filter) {
|
<ColorMatrix matrix={getFilterMatrix(filter)} style={styles.pageImage}>
|
||||||
case 'grayscale': return styles.filterGrayscale;
|
<Image source={{uri: page.uri}} style={styles.pageImage} resizeMode="contain" />
|
||||||
case 'blackwhite': return styles.filterBW;
|
</ColorMatrix>
|
||||||
case 'enhanced': return styles.filterEnhanced;
|
);
|
||||||
default: return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, {backgroundColor: t.bg}]}>
|
<View style={[styles.container, {backgroundColor: t.bg}]}>
|
||||||
@@ -63,8 +62,8 @@ export default function ReviewScreen({pages, onSave, onAddMore, onCancel}: Props
|
|||||||
}}>
|
}}>
|
||||||
{pages.map(page => (
|
{pages.map(page => (
|
||||||
<View key={page.id} style={styles.pageContainer}>
|
<View key={page.id} style={styles.pageContainer}>
|
||||||
<View style={[styles.imageWrapper, imageContainerStyle()]}>
|
<View style={styles.imageWrapper}>
|
||||||
<Image source={{uri: page.uri}} style={styles.pageImage} resizeMode="contain" />
|
{renderImage(page)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
@@ -128,9 +127,6 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
imageWrapper: {flex: 1, width: '100%', borderRadius: 8, overflow: 'hidden'},
|
imageWrapper: {flex: 1, width: '100%', borderRadius: 8, overflow: 'hidden'},
|
||||||
pageImage: {flex: 1, width: '100%'},
|
pageImage: {flex: 1, width: '100%'},
|
||||||
filterGrayscale: {opacity: 0.9},
|
|
||||||
filterBW: {opacity: 1},
|
|
||||||
filterEnhanced: {opacity: 1},
|
|
||||||
dots: {flexDirection: 'row', justifyContent: 'center', gap: 6, paddingVertical: 8},
|
dots: {flexDirection: 'row', justifyContent: 'center', gap: 6, paddingVertical: 8},
|
||||||
dot: {width: 6, height: 6, borderRadius: 3},
|
dot: {width: 6, height: 6, borderRadius: 3},
|
||||||
dotActive: {backgroundColor: '#007AFF', width: 18},
|
dotActive: {backgroundColor: '#007AFF', width: 18},
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ import {
|
|||||||
TextInput,
|
TextInput,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import Share from 'react-native-share';
|
import Share from 'react-native-share';
|
||||||
|
import {ColorMatrix} from 'react-native-color-matrix-image-filters';
|
||||||
import {ScannedDocument} from '../types';
|
import {ScannedDocument} from '../types';
|
||||||
import {generatePdf} from '../utils/generatePdf';
|
import {generatePdf} from '../utils/generatePdf';
|
||||||
|
import {getFilterMatrix} from '../utils/filterMatrices';
|
||||||
import {useTheme} from '../theme';
|
import {useTheme} from '../theme';
|
||||||
|
|
||||||
const {width: SW} = Dimensions.get('window');
|
const {width: SW} = Dimensions.get('window');
|
||||||
@@ -44,7 +46,7 @@ export default function ViewerScreen({document, onBack, onDelete, onRename}: Pro
|
|||||||
const label = pageIndices?.length === 1
|
const label = pageIndices?.length === 1
|
||||||
? `${document.name} — page ${pageIndices[0] + 1}`
|
? `${document.name} — page ${pageIndices[0] + 1}`
|
||||||
: document.name;
|
: document.name;
|
||||||
const pdfPath = await generatePdf(pages, label);
|
const pdfPath = await generatePdf(pages, label, document.filter);
|
||||||
await Share.open({
|
await Share.open({
|
||||||
title: label,
|
title: label,
|
||||||
type: 'application/pdf',
|
type: 'application/pdf',
|
||||||
@@ -98,11 +100,13 @@ export default function ViewerScreen({document, onBack, onDelete, onRename}: Pro
|
|||||||
}}>
|
}}>
|
||||||
{document.pages.map((page, i) => (
|
{document.pages.map((page, i) => (
|
||||||
<View key={page.id} style={styles.pageContainer}>
|
<View key={page.id} style={styles.pageContainer}>
|
||||||
|
<ColorMatrix matrix={getFilterMatrix(document.filter)} style={styles.pageImage}>
|
||||||
<Image
|
<Image
|
||||||
source={{uri: page.uri}}
|
source={{uri: page.uri}}
|
||||||
style={styles.pageImage}
|
style={styles.pageImage}
|
||||||
resizeMode="contain"
|
resizeMode="contain"
|
||||||
/>
|
/>
|
||||||
|
</ColorMatrix>
|
||||||
<Text style={[styles.pageNum, {color: t.textSecondary}]}>Page {i + 1}</Text>
|
<Text style={[styles.pageNum, {color: t.textSecondary}]}>Page {i + 1}</Text>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export interface ScannedDocument {
|
|||||||
pages: ScannedPage[];
|
pages: ScannedPage[];
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
name: string;
|
name: string;
|
||||||
|
filter: FilterMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScannedPage {
|
export interface ScannedPage {
|
||||||
|
|||||||
15
src/utils/filterMatrices.ts
Normal file
15
src/utils/filterMatrices.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import {concatColorMatrices, saturate, contrast, brightness, normal} from 'react-native-color-matrix-image-filters';
|
||||||
|
import {FilterMode} from '../types';
|
||||||
|
|
||||||
|
export function getFilterMatrix(filter: FilterMode): number[] {
|
||||||
|
switch (filter) {
|
||||||
|
case 'grayscale':
|
||||||
|
return saturate(0);
|
||||||
|
case 'blackwhite':
|
||||||
|
return concatColorMatrices(saturate(0), contrast(4));
|
||||||
|
case 'enhanced':
|
||||||
|
return concatColorMatrices(contrast(1.3), brightness(1.1));
|
||||||
|
default:
|
||||||
|
return normal();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import {NativeModules} from 'react-native';
|
import {NativeModules} from 'react-native';
|
||||||
import {ScannedPage} from '../types';
|
import {ScannedPage, FilterMode} from '../types';
|
||||||
|
import {getFilterMatrix} from './filterMatrices';
|
||||||
|
|
||||||
const {PdfGenerator} = NativeModules;
|
const {PdfGenerator} = NativeModules;
|
||||||
|
|
||||||
export async function generatePdf(pages: ScannedPage[], name: string): Promise<string> {
|
export async function generatePdf(pages: ScannedPage[], name: string, filter: FilterMode = 'original'): Promise<string> {
|
||||||
const imagePaths = pages.map(p => p.uri);
|
const imagePaths = pages.map(p => p.uri);
|
||||||
const fileName = name.replace(/[^a-z0-9_\-]/gi, '_');
|
const fileName = name.replace(/[^a-z0-9_\-]/gi, '_');
|
||||||
return PdfGenerator.generate(imagePaths, fileName);
|
return PdfGenerator.generate(imagePaths, fileName, getFilterMatrix(filter));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user