diff options
Diffstat (limited to 'compiler/src/Processors.hs')
-rw-r--r-- | compiler/src/Processors.hs | 201 |
1 files changed, 201 insertions, 0 deletions
diff --git a/compiler/src/Processors.hs b/compiler/src/Processors.hs new file mode 100644 index 0000000..159a425 --- /dev/null +++ b/compiler/src/Processors.hs | |||
@@ -0,0 +1,201 @@ | |||
1 | -- ldgallery - A static generator which turns a collection of tagged | ||
2 | -- pictures into a searchable web gallery. | ||
3 | -- | ||
4 | -- Copyright (C) 2019-2020 Pacien TRAN-GIRARD | ||
5 | -- | ||
6 | -- This program is free software: you can redistribute it and/or modify | ||
7 | -- it under the terms of the GNU Affero General Public License as | ||
8 | -- published by the Free Software Foundation, either version 3 of the | ||
9 | -- License, or (at your option) any later version. | ||
10 | -- | ||
11 | -- This program is distributed in the hope that it will be useful, | ||
12 | -- but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
13 | -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
14 | -- GNU Affero General Public License for more details. | ||
15 | -- | ||
16 | -- You should have received a copy of the GNU Affero General Public License | ||
17 | -- along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
18 | |||
19 | module Processors | ||
20 | ( Resolution(..) | ||
21 | , ItemFileProcessor, itemFileProcessor | ||
22 | , ThumbnailFileProcessor, thumbnailFileProcessor | ||
23 | , skipCached, withCached | ||
24 | ) where | ||
25 | |||
26 | |||
27 | import Control.Exception (Exception, throwIO) | ||
28 | import Data.Function ((&)) | ||
29 | import Data.Ratio ((%)) | ||
30 | import Data.Char (toLower) | ||
31 | |||
32 | import System.Directory hiding (copyFile) | ||
33 | import qualified System.Directory | ||
34 | import System.FilePath | ||
35 | |||
36 | import Codec.Picture | ||
37 | import Codec.Picture.Extra -- TODO: compare DCT and bilinear (and Lanczos, but it's not implemented) | ||
38 | |||
39 | import Resource | ||
40 | ( ItemProcessor, ThumbnailProcessor | ||
41 | , GalleryItemProps(..), Resolution(..) ) | ||
42 | |||
43 | import Files | ||
44 | |||
45 | |||
46 | data ProcessingException = ProcessingException FilePath String deriving Show | ||
47 | instance Exception ProcessingException | ||
48 | |||
49 | |||
50 | data PictureFileFormat = Bmp | Jpg | Png | Tiff | Hdr | Gif | ||
51 | |||
52 | -- TODO: handle video, music, text... | ||
53 | data Format = PictureFormat PictureFileFormat | Unknown | ||
54 | |||
55 | formatFromPath :: Path -> Format | ||
56 | formatFromPath = | ||
57 | maybe Unknown fromExt | ||
58 | . fmap (map toLower) | ||
59 | . fmap takeExtension | ||
60 | . fileName | ||
61 | where | ||
62 | fromExt :: String -> Format | ||
63 | fromExt ".bmp" = PictureFormat Bmp | ||
64 | fromExt ".jpg" = PictureFormat Jpg | ||
65 | fromExt ".jpeg" = PictureFormat Jpg | ||
66 | fromExt ".png" = PictureFormat Png | ||
67 | fromExt ".tiff" = PictureFormat Tiff | ||
68 | fromExt ".hdr" = PictureFormat Hdr | ||
69 | fromExt ".gif" = PictureFormat Gif | ||
70 | fromExt _ = Unknown | ||
71 | |||
72 | |||
73 | type FileProcessor = | ||
74 | FileName -- ^ Input path | ||
75 | -> FileName -- ^ Output path | ||
76 | -> IO () | ||
77 | |||
78 | copyFileProcessor :: FileProcessor | ||
79 | copyFileProcessor inputPath outputPath = | ||
80 | (putStrLn $ "Copying:\t" ++ outputPath) | ||
81 | >> ensureParentDir (flip System.Directory.copyFile) outputPath inputPath | ||
82 | |||
83 | resizeStaticImageUpTo :: PictureFileFormat -> Resolution -> FileProcessor | ||
84 | resizeStaticImageUpTo Bmp = resizeStaticGeneric readBitmap saveBmpImage | ||
85 | -- TODO: parameterise export quality for jpg | ||
86 | resizeStaticImageUpTo Jpg = resizeStaticGeneric readJpeg (saveJpgImage 80) | ||
87 | resizeStaticImageUpTo Png = resizeStaticGeneric readPng savePngImage | ||
88 | resizeStaticImageUpTo Tiff = resizeStaticGeneric readTiff saveTiffImage | ||
89 | resizeStaticImageUpTo Hdr = resizeStaticGeneric readHDR saveRadianceImage | ||
90 | resizeStaticImageUpTo Gif = resizeStaticGeneric readGif saveGifImage' | ||
91 | where | ||
92 | saveGifImage' :: StaticImageWriter | ||
93 | saveGifImage' outputPath image = | ||
94 | saveGifImage outputPath image | ||
95 | & either (throwIO . ProcessingException outputPath) id | ||
96 | |||
97 | |||
98 | type StaticImageReader = FilePath -> IO (Either String DynamicImage) | ||
99 | type StaticImageWriter = FilePath -> DynamicImage -> IO () | ||
100 | |||
101 | resizeStaticGeneric :: StaticImageReader -> StaticImageWriter -> Resolution -> FileProcessor | ||
102 | resizeStaticGeneric reader writer maxRes inputPath outputPath = | ||
103 | (putStrLn $ "Generating:\t" ++ outputPath) | ||
104 | >> reader inputPath | ||
105 | >>= either (throwIO . ProcessingException inputPath) return | ||
106 | >>= return . (fitDynamicImage maxRes) | ||
107 | >>= ensureParentDir writer outputPath | ||
108 | |||
109 | fitDynamicImage :: Resolution -> DynamicImage -> DynamicImage | ||
110 | fitDynamicImage (Resolution boxWidth boxHeight) image = | ||
111 | convertRGBA8 image | ||
112 | & scaleBilinear targetWidth targetHeight | ||
113 | & ImageRGBA8 | ||
114 | where | ||
115 | picWidth = dynamicMap imageWidth image | ||
116 | picHeight = dynamicMap imageHeight image | ||
117 | resizeRatio = min (boxWidth % picWidth) (boxHeight % picHeight) | ||
118 | targetWidth = floor $ resizeRatio * (picWidth % 1) | ||
119 | targetHeight = floor $ resizeRatio * (picHeight % 1) | ||
120 | |||
121 | |||
122 | type Cache = FileProcessor -> FileProcessor | ||
123 | |||
124 | skipCached :: Cache | ||
125 | skipCached processor inputPath outputPath = | ||
126 | removePathForcibly outputPath | ||
127 | >> processor inputPath outputPath | ||
128 | |||
129 | withCached :: Cache | ||
130 | withCached processor inputPath outputPath = | ||
131 | do | ||
132 | isDir <- doesDirectoryExist outputPath | ||
133 | if isDir then removePathForcibly outputPath else noop | ||
134 | |||
135 | fileExists <- doesFileExist outputPath | ||
136 | if fileExists then | ||
137 | do | ||
138 | needUpdate <- isOutdated True inputPath outputPath | ||
139 | if needUpdate then update else skip | ||
140 | else | ||
141 | update | ||
142 | |||
143 | where | ||
144 | noop = return () | ||
145 | update = processor inputPath outputPath | ||
146 | skip = putStrLn $ "Skipping:\t" ++ outputPath | ||
147 | |||
148 | |||
149 | type ItemFileProcessor = | ||
150 | FileName -- ^ Input base path | ||
151 | -> FileName -- ^ Output base path | ||
152 | -> FileName -- ^ Output class (subdir) | ||
153 | -> ItemProcessor | ||
154 | |||
155 | itemFileProcessor :: Maybe Resolution -> Cache -> ItemFileProcessor | ||
156 | itemFileProcessor maxResolution cached inputBase outputBase resClass inputRes = | ||
157 | cached processor inPath outPath | ||
158 | >> return (props relOutPath) | ||
159 | where | ||
160 | relOutPath = resClass /> inputRes | ||
161 | inPath = localPath $ inputBase /> inputRes | ||
162 | outPath = localPath $ outputBase /> relOutPath | ||
163 | (processor, props) = processorFor maxResolution $ formatFromPath inputRes | ||
164 | |||
165 | processorFor :: Maybe Resolution -> Format -> (FileProcessor, Path -> GalleryItemProps) | ||
166 | processorFor Nothing _ = | ||
167 | (copyFileProcessor, Other) | ||
168 | processorFor _ (PictureFormat Gif) = | ||
169 | (copyFileProcessor, Picture) -- TODO: handle animated gif resizing | ||
170 | processorFor (Just maxRes) (PictureFormat picFormat) = | ||
171 | (resizeStaticImageUpTo picFormat maxRes, Picture) | ||
172 | processorFor _ Unknown = | ||
173 | (copyFileProcessor, Other) -- TODO: handle video reencoding and others? | ||
174 | |||
175 | |||
176 | type ThumbnailFileProcessor = | ||
177 | FileName -- ^ Input base path | ||
178 | -> FileName -- ^ Output base path | ||
179 | -> FileName -- ^ Output class (subdir) | ||
180 | -> ThumbnailProcessor | ||
181 | |||
182 | thumbnailFileProcessor :: Resolution -> Cache -> ThumbnailFileProcessor | ||
183 | thumbnailFileProcessor maxRes cached inputBase outputBase resClass inputRes = | ||
184 | cached <$> processorFor (formatFromPath inputRes) | ||
185 | & process | ||
186 | where | ||
187 | relOutPath = resClass /> inputRes | ||
188 | inPath = localPath $ inputBase /> inputRes | ||
189 | outPath = localPath $ outputBase /> relOutPath | ||
190 | |||
191 | process :: Maybe FileProcessor -> IO (Maybe Path) | ||
192 | process Nothing = return Nothing | ||
193 | process (Just proc) = | ||
194 | proc inPath outPath | ||
195 | >> return (Just relOutPath) | ||
196 | |||
197 | processorFor :: Format -> Maybe FileProcessor | ||
198 | processorFor (PictureFormat picFormat) = | ||
199 | Just $ resizeStaticImageUpTo picFormat maxRes | ||
200 | processorFor _ = | ||
201 | Nothing | ||