diff --git a/jpeger/.gitignore b/jpeger/.gitignore new file mode 100644 index 0000000..8a02982 --- /dev/null +++ b/jpeger/.gitignore @@ -0,0 +1,3 @@ +jpeger +jpeger.exe +*.log diff --git a/jpeger/Makefile b/jpeger/Makefile new file mode 100644 index 0000000..849ed1a --- /dev/null +++ b/jpeger/Makefile @@ -0,0 +1,10 @@ +.PHONY: build windows clean + +build: + go build -o jpeger main.go + +windows: + docker run --rm -v "$(shell pwd)":/usr/src/myapp -w /usr/src/myapp -e GOOS=windows -e GOARCH=amd64 golang:1.16 go build -v -o jpeger.exe main.go + +clean: + rm -rvf jpeger* diff --git a/jpeger/go.mod b/jpeger/go.mod new file mode 100644 index 0000000..b8ae81b --- /dev/null +++ b/jpeger/go.mod @@ -0,0 +1,5 @@ +module github.com/max747/fgojunks/jpeger + +go 1.16 + +require golang.org/x/text v0.3.6 // indirect diff --git a/jpeger/go.sum b/jpeger/go.sum new file mode 100644 index 0000000..3c12b4f --- /dev/null +++ b/jpeger/go.sum @@ -0,0 +1,3 @@ +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/jpeger/main.go b/jpeger/main.go new file mode 100644 index 0000000..93c9657 --- /dev/null +++ b/jpeger/main.go @@ -0,0 +1,292 @@ +package main + +import ( + "archive/zip" + "bytes" + "fmt" + "image" + "image/jpeg" + _ "image/png" + "io" + "io/fs" + "log" + "os" + "path/filepath" + "runtime" + "strings" + + "golang.org/x/text/encoding/japanese" +) + +const ( + program = "jpeger" + version = "0.3.0" + logName = "jpeger.log" + + success = 0 + failure = 1 + + jpegQuality = 90 +) + +var logger *log.Logger + +func processImage(data io.ReadCloser) (*bytes.Buffer, error) { + im, format, err := image.Decode(data) + if err != nil { + return nil, fmt.Errorf("image.Decode: %w", err) + } + logger.Printf(" %s %v", format, im.Bounds()) + + buf := new(bytes.Buffer) + options := &jpeg.Options{Quality: jpegQuality} + if err := jpeg.Encode(buf, im, options); err != nil { + return nil, fmt.Errorf("jpeg.Encode: %w", err) + } + return buf, nil +} + +func processImageFile(srcPath, destPath string) error { + r, err := os.Open(srcPath) + if err != nil { + return fmt.Errorf("os.Open: %w", err) + } + defer r.Close() + + w, err := os.Create(destPath) + if err != nil { + return fmt.Errorf("os.Create: %w", err) + } + defer w.Close() + + buf, err := processImage(r) + if err != nil { + return fmt.Errorf("processImage: %w", err) + } + + _, err = w.Write(buf.Bytes()) + if err != nil { + return fmt.Errorf("w.Write: %w", err) + } + return nil +} + +func processZipItem(zf *zip.File) (*bytes.Buffer, error) { + rc, err := zf.Open() + if err != nil { + return nil, fmt.Errorf("zf.Open: %w", err) + } + defer rc.Close() + + buf, err := processImage(rc) + if err != nil { + return nil, fmt.Errorf("processImage: %w", err) + } + return buf, nil +} + +func convertZipItems(srcPath, destPath string) error { + r, err := zip.OpenReader(srcPath) + if err != nil { + return fmt.Errorf("zip.OpenReader: %w", err) + } + defer r.Close() + + zipbuf := new(bytes.Buffer) + w := zip.NewWriter(zipbuf) + + for _, f := range r.File { + logger.Printf(" %s\n", f.Name) + buf, err := processZipItem(f) + if err != nil { + // エラーでも中断せずに継続する + logger.Printf("processZipFile: %v\n", err) + logger.Printf("skip processing %s\n", f.Name) + continue + } + stem, _ := splitExt(f.Name) + outputName := fmt.Sprintf("%s.jpg", stem) + logger.Printf(" => %s\n", outputName) + wf, err := w.Create(outputName) + if err != nil { + return fmt.Errorf("w.Create: %w", err) + } + if _, err := wf.Write(buf.Bytes()); err != nil { + return fmt.Errorf("wf.Write: %w", err) + } + } + + out, err := os.Create(destPath) + if err != nil { + return fmt.Errorf("os.Create: %w", err) + } + defer out.Close() + + w.Close() // zipbuf を読みだす前に終端処理が必要 + if _, err := out.Write(zipbuf.Bytes()); err != nil { + return fmt.Errorf("out.Write: %w", err) + } + return nil +} + +func splitExt(src string) (stem, ext string) { + ext = filepath.Ext(src) + stem = src[:len(src)-len(ext)] + return +} + +func decodePathStrings(src string) (string, error) { + switch runtime.GOOS { + case "windows": + decoder := japanese.ShiftJIS.NewDecoder() + utf8str, err := decoder.String(src) + if err != nil { + return src, fmt.Errorf("decoder.String: %w", err) + } + return utf8str, nil + default: + return src, nil + } +} + +func resolveDestPath(srcPath string) string { + srcStem, srcExt := splitExt(srcPath) + switch strings.ToLower(srcExt) { + case ".png": + return fmt.Sprintf("%s.jpg", srcStem) + + case ".jpg", ".jpeg": + return fmt.Sprintf("%s%s", srcStem, srcExt) + + default: + return fmt.Sprintf("%s_jpeg%s", srcStem, srcExt) + } +} + +func copyFile(src, dest string) error { + r, err := os.Open(src) + if err != nil { + return fmt.Errorf("os.Open: %w", err) + } + defer r.Close() + + w, err := os.Create(dest) + if err != nil { + return fmt.Errorf("os.Create: %w", err) + } + defer w.Close() + + if _, err := io.Copy(w, r); err != nil { + return fmt.Errorf("io.Copy: %w", err) + } + return nil +} + +func runUnit(srcPath, destPath string, skipJpeg bool) error { + _, srcExt := splitExt(srcPath) + switch strings.ToLower(srcExt) { + case ".png": + if err := processImageFile(srcPath, destPath); err != nil { + return fmt.Errorf("processImageFile: %w", err) + } + case ".jpg", ".jpeg": + if skipJpeg { + logger.Printf("skip processing jpeg file: %s\n", srcPath) + return nil + } + + if err := copyFile(srcPath, destPath); err != nil { + return fmt.Errorf("copyFile: %w", err) + } + + case ".zip": + if err := convertZipItems(srcPath, destPath); err != nil { + return fmt.Errorf("convertZipItems: %w", err) + } + default: + return fmt.Errorf("unsupported file type: %s", srcExt) + } + return nil +} + +func run() int { + logFile, err := os.OpenFile(logName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + log.Fatal(err) + return failure + } + defer logFile.Close() + multiWriter := io.MultiWriter(os.Stdout, logFile) + + logger = log.New(multiWriter, "", log.LstdFlags) + logger.Printf("<<< %s %s %s/%s >>>\n", program, version, runtime.GOOS, runtime.GOARCH) + logger.Printf("args: %v\n", os.Args) + + if len(os.Args) < 2 { + logger.Println("too few arguments") + return failure + } + + srcPath := os.Args[1] + stat, err := os.Stat(srcPath) + if os.IsNotExist(err) { + logger.Printf("%s: no such file or directory\n", srcPath) + return failure + } + srcIsDir := stat.IsDir() + + srcStem, srcExt := splitExt(srcPath) + logger.Printf("stem: %s, ext: %s, isdir: %v\n", srcStem, srcExt, srcIsDir) + + destPath := resolveDestPath(srcPath) + logger.Printf("dest: %s\n", destPath) + + if srcIsDir { + logger.Printf("start to walk on: %s\n", srcPath) + if err := filepath.Walk(srcPath, func(child string, info fs.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("path %s; err: %w", child, err) + } + logger.Printf(" | %s\n", child) + + if info.IsDir() { + destDir := strings.Replace(child, srcPath, destPath, 1) + if err := os.Mkdir(destDir, os.ModeDir); err != nil { + return fmt.Errorf("os.Mkdir: %w", err) + } + return nil + } + + parent, childName := filepath.Split(child) + destName := resolveDestPath(childName) + destParent := strings.Replace(parent, srcPath, destPath, 1) + dest := filepath.Join(destParent, destName) + logger.Printf(" | dest: %s\n", dest) + + // jpeg は単にコピー + if err := runUnit(child, dest, false); err != nil { + logger.Printf("runUnit: %s\n", err) + // 単発の処理でエラーが発生しても止めずに続行 + } + return nil + + }); err != nil { + logger.Printf("filePath.Walk: %s\n", err) + return failure + } + + } else { + // jpeg は無視 + if err := runUnit(srcPath, destPath, true); err != nil { + logger.Printf("runUnit: %s\n", err) + return failure + } + } + + logger.Println("done!") + return success +} + +func main() { + os.Exit(run()) +} diff --git a/pageinfo/.gitignore b/pageinfo/.gitignore deleted file mode 100644 index e5dbb11..0000000 --- a/pageinfo/.gitignore +++ /dev/null @@ -1 +0,0 @@ -debugimages/ diff --git a/pageinfo/pageinfo.py b/pageinfo/pageinfo.py deleted file mode 100755 index a63eda0..0000000 --- a/pageinfo/pageinfo.py +++ /dev/null @@ -1,435 +0,0 @@ -#!/usr/bin/env python3 -# -# MIT License -# Copyright 2020 max747 -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -import argparse -import csv -import logging -import os -import sys - -import cv2 - -logger = logging.getLogger('fgo') - -NOSCROLL_PAGE_INFO = (1, 1, 0) - - -class PageInfoError(Exception): - pass - - -class CannotGuessError(PageInfoError): - pass - - -class TooManyAreasDetectedError(PageInfoError): - pass - - -class ScrollableAreaNotFoundError(PageInfoError): - pass - - -def filter_contour_qp(contour, im): - """ - "所持 QP" エリアを拾い、それ以外を除外するフィルター - """ - im_h, im_w = im.shape[:2] - # 画像全体に対する検出領域の面積比が一定以上であること。 - # 明らかに小さすぎる領域はここで捨てる。 - if cv2.contourArea(contour) * 25 < im_w * im_h: - return False - x, y, w, h = cv2.boundingRect(contour) - # 横長領域なので、高さに対して十分大きい幅になっていること。 - if w < h * 6: - return False - # 横幅が画像サイズに対して長すぎず短すぎないこと。 - # 長すぎる場合は画面下部の端末別表示調整用領域を検出している可能性がある。 - if not (w * 1.2 < im_w < w * 2): - return False - logger.debug('qp region: (x, y, width, height) = (%s, %s, %s, %s)', x, y, w, h) - return True - - -def filter_contour_scrollbar(contour, im): - """ - スクロールバー領域を拾い、それ以外を除外するフィルター - """ - im_h, im_w = im.shape[:2] - # 画像全体に対する検出領域の面積比が一定以上であること。 - # 明らかに小さすぎる領域はここで捨てる。 - if cv2.contourArea(contour) * 80 < im_w * im_h: - return False - x, y, w, h = cv2.boundingRect(contour) - logger.debug('scrollbar candidate: (x, y, width, height) = (%s, %s, %s, %s)', x, y, w, h) - # 縦長領域なので、幅に対して十分大きい高さになっていること。 - if h < w * 5: - return False - logger.debug('scrollbar region: (x, y, width, height) = (%s, %s, %s, %s)', x, y, w, h) - return True - - -def filter_contour_scrollable_area(contour, im): - """ - スクロール可能領域を拾い、それ以外を除外するフィルター - """ - im_w, im_h = im.shape[:2] - # 画像全体に対する検出領域の面積比が一定以上であること。 - # 明らかに小さすぎる領域はここで捨てる。 - if cv2.contourArea(contour) * 50 < im_w * im_h: - return False - x, y, w, h = cv2.boundingRect(contour) - logger.debug('scrollable area candidate: (x, y, width, height) = (%s, %s, %s, %s)', x, y, w, h) - # 縦長領域なので、幅に対して十分大きい高さになっていること。 - if h < w * 10: - return False - logger.debug('scrollable area region: (x, y, width, height) = (%s, %s, %s, %s)', x, y, w, h) - return True - - -def detect_qp_region(im, debug_draw_image=False, debug_image_name=None): - """ - "所持 QP" 領域を検出する - """ - # 縦横2分割して4領域に分け、左下の領域だけ使う。 - # QP の領域を調べたいならそれで十分。 - im_h, im_w = im.shape[:2] - cropped = im[int(im_h/2):im_h, 0:int(im_w/2)] - cr_h, cr_w = cropped.shape[:2] - logger.debug('cropped image size (for qp): (width, height) = (%s, %s)', cr_w, cr_h) - im_gray = cv2.cvtColor(cropped, cv2.COLOR_BGR2GRAY) - binary_threshold = 25 - ret, th1 = cv2.threshold(im_gray, binary_threshold, 255, cv2.THRESH_BINARY) - contours, hierarchy = cv2.findContours(th1, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - - filtered_contours = [c for c in contours if filter_contour_qp(c, im_gray)] - if len(filtered_contours) == 1: - qp_region = filtered_contours[0] - x, y, w, h = cv2.boundingRect(qp_region) - # 左右の無駄領域を除外する。 - # 感覚的な値ではあるが 左 12%, 右 7% を除外。 - topleft = (x + int(w*0.12), y) - bottomright = (topleft[0] + w - int(w*0.12) - int(w*0.07), y + h) - - # TODO 切り出した領域をどう扱うか決めてない - - if debug_draw_image: - cv2.rectangle(cropped, topleft, bottomright, (0, 0, 255), 3) - - if debug_draw_image: - cv2.drawContours(cropped, filtered_contours, -1, (0, 255, 0), 3) - logger.debug('writing debug image: %s', debug_image_name) - cv2.imwrite(debug_image_name, cropped) - - -def guess_pages(actual_width, actual_height, entire_width, entire_height): - """ - スクロールバー領域の高さからドロップ枠が何ページあるか推定する - """ - delta = abs(entire_width - actual_width) - if delta > 9: - # 比較しようとしている領域が異なる可能性が高い - raise CannotGuessError( - f'幅の誤差が大きすぎます: delta = {delta}, ' - f'entire_width = {entire_width}, ' - f'actual_width = {actual_width}' - ) - - if actual_height * 1.1 > entire_height: - return 1 - if actual_height * 2.2 > entire_height: - return 2 - # 4 ページ以上 (ドロップ枠総数 > 63) になることはないと仮定。 - return 3 - - -def guess_pagenum(actual_x, actual_y, entire_x, entire_y, entire_height): - """ - スクロールバー領域の y 座標の位置からドロップ画像のページ数を推定する - """ - if abs(actual_x - entire_x) > 9: - # 比較しようとしている領域が異なる可能性が高い - raise CannotGuessError(f'x 座標の誤差が大きすぎます: entire_x = {entire_x}, actual_x = {actual_x}') - - # スクロールバーと上端との空き領域の縦幅 delta と - # スクロール可能領域の縦幅 entire_height との比率で位置を推定する。 - delta = actual_y - entire_y - ratio = delta / entire_height - logger.debug('space above scrollbar: %s, entire_height: %s, ratio: %s', delta, entire_height, ratio) - if ratio < 0.1: - return 1 - # 実測では 0.47-0.50 の間くらいになる。 - # 7列3ページの3ページ目の値が 0.55 近辺なので、あまり余裕を持たせて大きくしすぎてもいけない。 - # このあたりから 0.52 くらいが妥当な線ではないか。 - if ratio < 0.52: - return 2 - # 4 ページ以上になることはないと仮定。 - return 3 - - -def guess_lines(actual_width, actual_height, entire_width, entire_height): - """ - スクロールバー領域の高さからドロップ枠が何行あるか推定する - スクロールバーを用いる関係上、原理的に 2 行以下は推定不可 - """ - delta = abs(entire_width - actual_width) - if delta > 9: - # 比較しようとしている領域が異なる可能性が高い - raise CannotGuessError( - f'幅の誤差が大きすぎます: delta = {delta}, ' - f'entire_width = {entire_width}, ' - f'actual_width = {actual_width}' - ) - - ratio = actual_height / entire_height - logger.debug('scrollbar ratio: %s', ratio) - if ratio > 0.90: # 実測値 0.94 - return 3 - elif ratio > 0.70: # 実測値 0.72-0.73 - return 4 - elif ratio > 0.57: # 実測値 0.59-0.60 - return 5 - elif ratio > 0.48: # 実測値 0.50-0.51 - return 6 - elif ratio > 0.40: # サンプルなし 参考値 1/2.333 = 0.429, 1/2.5 = 0.4 - return 7 - elif ratio > 0.36: # サンプルなし 参考値 1/2.666 = 0.375, 1/2.77 = 0.361 - return 8 - else: - # 10 行以上は考慮しない - return 9 - - -def _detect_scrollbar_region(im, binary_threshold, filter_func): - ret, th1 = cv2.threshold(im, binary_threshold, 255, cv2.THRESH_BINARY) - contours, hierarchy = cv2.findContours(th1, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - return [c for c in contours if filter_func(c, im)] - - -def _is_same_contour(contour0, contour1): - x0, y0, w0, h0 = cv2.boundingRect(contour0) - x1, y1, w1, h1 = cv2.boundingRect(contour1) - if abs(x1 - x0) > 1: - return False - elif abs(y1 - y0) > 1: - return False - elif abs(w1 - w0) > 1: - return False - elif abs(h1 - h0) > 1: - return False - return True - - -def _try_to_detect_scrollbar(im_gray, im_orig_for_debug=None): - """ - スクロールバーおよびスクロール可能領域の検出 - - debug 画像を出力したい場合は im_orig_for_debug に二値化 - される前の元画像 (crop されたもの) を渡すこと。 - """ - # 二値化の閾値を高めにするとスクロールバー本体の領域を検出できる。 - # 低めにするとスクロールバー可能領域を検出できる。 - threshold_for_actual = 60 - # スクロール可能領域の判定は、単一の閾値ではどうやっても PNG/JPEG の - # 両方に対応するのが難しい。そこで、閾値にレンジを設けて高い方から順に - # トライしていく。閾値が低くなるほど検出されやすいが、矩形がゆがみ - # やすくなり、後の誤検出につながる。そのため、高い閾値で検出できれば - # それを正とするのがよい。 - thresholds_for_entire = (25, 24, 23) - - actual_scrollbar_contours = _detect_scrollbar_region( - im_gray, threshold_for_actual, filter_contour_scrollbar) - if len(actual_scrollbar_contours) == 0: - return (None, None) - - if im_orig_for_debug is not None: - cv2.drawContours(im_orig_for_debug, actual_scrollbar_contours, -1, (0, 255, 0), 3) - - if len(actual_scrollbar_contours) > 1: - n = len(actual_scrollbar_contours) - raise TooManyAreasDetectedError(f'{n} actual scrollbar areas are detected') - - actual_scrollbar_contour = actual_scrollbar_contours[0] - - scrollable_area_contour = None - for th in thresholds_for_entire: - scrollable_area_contours = _detect_scrollbar_region( - im_gray, th, filter_contour_scrollable_area) - if len(scrollable_area_contours) == 0: - logger.debug(f'th {th}: scrollbar was found, but scrollable area is not found, retry') - continue - - if len(scrollable_area_contours) > 1: - if im_orig_for_debug is not None: - cv2.drawContours(im_orig_for_debug, scrollable_area_contours, -1, (255, 0, 0), 3) - - n = len(scrollable_area_contours) - raise TooManyAreasDetectedError(f'{n} scrollable areas are detected') - - scrollable_area_contour = scrollable_area_contours[0] - same_contour = _is_same_contour(actual_scrollbar_contour, scrollable_area_contour) - if same_contour: - # 同じ領域を検出してしまっている場合、誤検出とみなして - # 閾値を下げてリトライする - logger.debug(f'th {th}: seems to detect scrollbar as scrollable area, retry') - continue - break - - if im_orig_for_debug is not None and scrollable_area_contour is not None: - cv2.drawContours(im_orig_for_debug, [scrollable_area_contour], -1, (255, 0, 0), 3) - - # thresholds_for_entire のすべての閾値でスクロール可能領域が検出できない - # 場合は、そもそも元のスクロールバーが誤認識であった可能性が出てくる。 - # この場合 scrollable_area_contour は None になるが、その場合は呼び出し - # 側でスクロールバー誤検出とみなすようにする。 - return actual_scrollbar_contour, scrollable_area_contour - - -def guess_pageinfo(im, debug_draw_image=False, debug_image_name=None): - """ - ページ情報を推定する。 - 返却値は (現ページ数, 全体ページ数, 全体行数) - スクロールバーがない場合は全体行数の推定は不可能。その場合は - NOSCROLL_PAGE_INFO すなわち (1, 1, 0) を返す - """ - # 縦4分割して4領域に分け、一番右の領域だけ使う。 - # スクロールバーの領域を調べたいならそれで十分。 - im_h, im_w = im.shape[:2] - cropped = im[0:im_h, int(im_w*3/4):im_w] - cr_h, cr_w = cropped.shape[:2] - logger.debug('cropped image size (for scrollbar): (width, height) = (%s, %s)', cr_w, cr_h) - im_gray = cv2.cvtColor(cropped, cv2.COLOR_BGR2GRAY) - - if debug_draw_image: - im_orig_for_debug = cropped - else: - im_orig_for_debug = None - - actual_scrollbar_region, scrollable_area_region = \ - _try_to_detect_scrollbar(im_gray, im_orig_for_debug) - - if debug_draw_image: - logger.debug('writing debug image: %s', debug_image_name) - cv2.imwrite(debug_image_name, cropped) - - if actual_scrollbar_region is None or scrollable_area_region is None: - # スクロールバーが検出できない or スクロールバー誤検出(と推定) - # どちらの場合もスクロールバーなしとして扱う。 - return NOSCROLL_PAGE_INFO - - asr_x, asr_y, asr_w, asr_h = cv2.boundingRect(actual_scrollbar_region) - esr_x, esr_y, esr_w, esr_h = cv2.boundingRect(scrollable_area_region) - - pages = guess_pages(asr_w, asr_h, esr_w, esr_h) - pagenum = guess_pagenum(asr_x, asr_y, esr_x, esr_y, esr_h) - lines = guess_lines(asr_w, asr_h, esr_w, esr_h) - return (pagenum, pages, lines) - - -def look_into_file(filename, args): - logger.debug(f'===== {filename}') - - im = cv2.imread(filename) - if im is None: - raise FileNotFoundError(f'Cannot read file: {filename}') - - im_h, im_w = im.shape[:2] - logger.debug('image size: (width, height) = (%s, %s)', im_w, im_h) - - # TODO QP 領域をどう扱うか未定 - # if args.debug_qp: - # debug_qp_dir = os.path.join(args.debug_out_dir, 'qp') - # os.makedirs(debug_qp_dir, exist_ok=True) - # debug_qp_image = os.path.join(debug_qp_dir, os.path.basename(filename)) - # else: - # debug_qp_image = None - # detect_qp_region(im, args.debug_qp, debug_qp_image) - - if args.debug_sc: - debug_sc_dir = os.path.join(args.debug_out_dir, 'sc') - os.makedirs(debug_sc_dir, exist_ok=True) - debug_sc_image = os.path.join(debug_sc_dir, os.path.basename(filename)) - else: - debug_sc_image = None - pagenum, pages, lines = guess_pageinfo(im, args.debug_sc, debug_sc_image) - logger.debug('pagenum: %s, pages: %s, lines: %s', pagenum, pages, lines) - return (pagenum, pages, lines) - - -def main(args): - csvdata = [] - - for filename in args.filename: - if os.path.isdir(filename): - for child in os.listdir(filename): - path = os.path.join(filename, child) - result = look_into_file(path, args) - csvdata.append((path, *result)) - else: - result = look_into_file(filename, args) - csvdata.append((filename, *result)) - - csv_writer = csv.writer(args.output, lineterminator='\n') - csv_writer.writerows(csvdata) - - -def parse_args(): - parser = argparse.ArgumentParser() - parser.add_argument('filename', nargs='+') - parser.add_argument( - '-l', '--loglevel', - choices=('debug', 'info', 'warning'), - default='info', - help='set loglevel [default: info]', - ) - # parser.add_argument( - # '-dq', '--debug-qp', - # action='store_true', - # help='enable writing qp image for debug', - # ) - parser.add_argument( - '-ds', '--debug-sc', - action='store_true', - help='enable writing sc image for debug', - ) - parser.add_argument( - '-do', '--debug-out-dir', - default='debugimages', - help='output directory for debug images [default: debugimages]', - ) - parser.add_argument( - '-o', '--output', - type=argparse.FileType('w'), - default=sys.stdout, - help='output file [default: STDOUT]', - ) - return parser.parse_args() - - -if __name__ == '__main__': - args = parse_args() - logging.basicConfig(level=logging.INFO, format='[%(levelname)s] %(message)s') - logger.setLevel(args.loglevel.upper()) - main(args) diff --git a/pageinfo/requirements.txt b/pageinfo/requirements.txt deleted file mode 100644 index d9c4fab..0000000 --- a/pageinfo/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -opencv-python -pytest diff --git a/pageinfo/runtest.bash b/pageinfo/runtest.bash deleted file mode 100755 index 364e707..0000000 --- a/pageinfo/runtest.bash +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -python -m unittest tests.pageinfo_test $@ diff --git a/pageinfo/tests/__init__.py b/pageinfo/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pageinfo/tests/images/000/000.png b/pageinfo/tests/images/000/000.png deleted file mode 100644 index 90bbe9d..0000000 Binary files a/pageinfo/tests/images/000/000.png and /dev/null differ diff --git a/pageinfo/tests/images/000/001.png b/pageinfo/tests/images/000/001.png deleted file mode 100644 index 10cf1f4..0000000 Binary files a/pageinfo/tests/images/000/001.png and /dev/null differ diff --git a/pageinfo/tests/images/000/002.png b/pageinfo/tests/images/000/002.png deleted file mode 100644 index 9cf0ff8..0000000 Binary files a/pageinfo/tests/images/000/002.png and /dev/null differ diff --git a/pageinfo/tests/images/000/003.png b/pageinfo/tests/images/000/003.png deleted file mode 100644 index 3ec9549..0000000 Binary files a/pageinfo/tests/images/000/003.png and /dev/null differ diff --git a/pageinfo/tests/images/000/004.png b/pageinfo/tests/images/000/004.png deleted file mode 100644 index c13fdf7..0000000 Binary files a/pageinfo/tests/images/000/004.png and /dev/null differ diff --git a/pageinfo/tests/images/000/005.png b/pageinfo/tests/images/000/005.png deleted file mode 100644 index ce5022b..0000000 Binary files a/pageinfo/tests/images/000/005.png and /dev/null differ diff --git a/pageinfo/tests/images/000/006.png b/pageinfo/tests/images/000/006.png deleted file mode 100644 index 473e8e3..0000000 Binary files a/pageinfo/tests/images/000/006.png and /dev/null differ diff --git a/pageinfo/tests/images/000/007.png b/pageinfo/tests/images/000/007.png deleted file mode 100644 index c8f7243..0000000 Binary files a/pageinfo/tests/images/000/007.png and /dev/null differ diff --git a/pageinfo/tests/images/001/000.png b/pageinfo/tests/images/001/000.png deleted file mode 100644 index 4d9c2a3..0000000 Binary files a/pageinfo/tests/images/001/000.png and /dev/null differ diff --git a/pageinfo/tests/images/001/001.png b/pageinfo/tests/images/001/001.png deleted file mode 100644 index 984198e..0000000 Binary files a/pageinfo/tests/images/001/001.png and /dev/null differ diff --git a/pageinfo/tests/images/001/002.png b/pageinfo/tests/images/001/002.png deleted file mode 100644 index 602db1a..0000000 Binary files a/pageinfo/tests/images/001/002.png and /dev/null differ diff --git a/pageinfo/tests/images/001/003.png b/pageinfo/tests/images/001/003.png deleted file mode 100644 index b38508b..0000000 Binary files a/pageinfo/tests/images/001/003.png and /dev/null differ diff --git a/pageinfo/tests/images/001/004.png b/pageinfo/tests/images/001/004.png deleted file mode 100644 index e0316c1..0000000 Binary files a/pageinfo/tests/images/001/004.png and /dev/null differ diff --git a/pageinfo/tests/images/001/005.png b/pageinfo/tests/images/001/005.png deleted file mode 100644 index fdd61fa..0000000 Binary files a/pageinfo/tests/images/001/005.png and /dev/null differ diff --git a/pageinfo/tests/images/002/000.png b/pageinfo/tests/images/002/000.png deleted file mode 100644 index 85937ac..0000000 Binary files a/pageinfo/tests/images/002/000.png and /dev/null differ diff --git a/pageinfo/tests/images/003/000.png b/pageinfo/tests/images/003/000.png deleted file mode 100644 index 7f6fc8e..0000000 Binary files a/pageinfo/tests/images/003/000.png and /dev/null differ diff --git a/pageinfo/tests/images/003/001.png b/pageinfo/tests/images/003/001.png deleted file mode 100644 index 277582a..0000000 Binary files a/pageinfo/tests/images/003/001.png and /dev/null differ diff --git a/pageinfo/tests/images/003/002.png b/pageinfo/tests/images/003/002.png deleted file mode 100644 index 77aa4d3..0000000 Binary files a/pageinfo/tests/images/003/002.png and /dev/null differ diff --git a/pageinfo/tests/images/003/003.png b/pageinfo/tests/images/003/003.png deleted file mode 100644 index cae42d5..0000000 Binary files a/pageinfo/tests/images/003/003.png and /dev/null differ diff --git a/pageinfo/tests/images/004/000.png b/pageinfo/tests/images/004/000.png deleted file mode 100755 index 4a86f06..0000000 Binary files a/pageinfo/tests/images/004/000.png and /dev/null differ diff --git a/pageinfo/tests/images/005/000.jpg b/pageinfo/tests/images/005/000.jpg deleted file mode 100644 index 09dfcea..0000000 Binary files a/pageinfo/tests/images/005/000.jpg and /dev/null differ diff --git a/pageinfo/tests/images/005/000.png b/pageinfo/tests/images/005/000.png deleted file mode 100644 index 7b04926..0000000 Binary files a/pageinfo/tests/images/005/000.png and /dev/null differ diff --git a/pageinfo/tests/images/005/001.jpg b/pageinfo/tests/images/005/001.jpg deleted file mode 100644 index 53c6a1b..0000000 Binary files a/pageinfo/tests/images/005/001.jpg and /dev/null differ diff --git a/pageinfo/tests/images/005/001.png b/pageinfo/tests/images/005/001.png deleted file mode 100644 index 02c38e9..0000000 Binary files a/pageinfo/tests/images/005/001.png and /dev/null differ diff --git a/pageinfo/tests/images/005/002.jpg b/pageinfo/tests/images/005/002.jpg deleted file mode 100644 index e295cc8..0000000 Binary files a/pageinfo/tests/images/005/002.jpg and /dev/null differ diff --git a/pageinfo/tests/images/005/003.jpg b/pageinfo/tests/images/005/003.jpg deleted file mode 100644 index 9e866dd..0000000 Binary files a/pageinfo/tests/images/005/003.jpg and /dev/null differ diff --git a/pageinfo/tests/images/005/003.png b/pageinfo/tests/images/005/003.png deleted file mode 100644 index 0a47b7b..0000000 Binary files a/pageinfo/tests/images/005/003.png and /dev/null differ diff --git a/pageinfo/tests/images/005/004.jpg b/pageinfo/tests/images/005/004.jpg deleted file mode 100644 index b038c86..0000000 Binary files a/pageinfo/tests/images/005/004.jpg and /dev/null differ diff --git a/pageinfo/tests/images/005/004.png b/pageinfo/tests/images/005/004.png deleted file mode 100644 index b75ac20..0000000 Binary files a/pageinfo/tests/images/005/004.png and /dev/null differ diff --git a/pageinfo/tests/pageinfo_test.py b/pageinfo/tests/pageinfo_test.py deleted file mode 100644 index fbd5155..0000000 --- a/pageinfo/tests/pageinfo_test.py +++ /dev/null @@ -1,107 +0,0 @@ -import os -import unittest -from logging import getLogger - -import cv2 - -import pageinfo - -logger = getLogger(__name__) -here = os.path.dirname(os.path.abspath(__file__)) - - -def get_images_absdir(dirname): - return os.path.join(here, 'images', dirname) - - -class PageinfoTest(unittest.TestCase): - def _test_guess_pageinfo(self, images_dir, expected): - for entry in os.listdir(images_dir): - impath = os.path.join(images_dir, entry) - with self.subTest(image=impath): - im = cv2.imread(impath) - logger.debug(impath) - try: - actual = pageinfo.guess_pageinfo(im) - self.assertEqual(actual, expected[entry]) - except pageinfo.CannotGuessError as e: - self.fail(f'{impath}: {e}') - - def test_guess_pageinfo_000(self): - images_dir = get_images_absdir('000') - expected = { - '000.png': (1, 1, 0), - '001.png': (1, 1, 0), - '002.png': (1, 1, 3), - '003.png': (1, 1, 3), - '004.png': (1, 2, 4), - '005.png': (2, 2, 4), - '006.png': (1, 2, 6), - '007.png': (2, 2, 6), - } - self._test_guess_pageinfo(images_dir, expected) - - def test_guess_pageinfo_001(self): - images_dir = get_images_absdir('001') - expected = { - '000.png': (1, 1, 3), - '001.png': (1, 2, 4), - '002.png': (2, 2, 4), - '003.png': (1, 1, 3), - '004.png': (1, 2, 4), - '005.png': (2, 2, 4), - } - self._test_guess_pageinfo(images_dir, expected) - - def test_guess_pageinfo_002(self): - images_dir = get_images_absdir('002') - expected = { - '000.png': (2, 2, 5), - } - self._test_guess_pageinfo(images_dir, expected) - - def test_guess_pageinfo_003(self): - """ - 2ページ目なのに3ページ目と判定される不具合を修正。 - """ - images_dir = get_images_absdir('003') - expected = { - '000.png': (2, 2, 6), - '001.png': (1, 3, 7), - '002.png': (2, 3, 7), - '003.png': (3, 3, 7), - } - self._test_guess_pageinfo(images_dir, expected) - - def test_guess_pageinfo_004(self): - """ - スクロールバーの誤検出により認識エラーになる件について、 - スクロール可能領域を検出できない場合はスクロールバー - なしと判定するようにした。 - https://github.com/max747/fgojunks/issues/1 - """ - images_dir = get_images_absdir('004') - expected = { - '000.png': (1, 1, 0), - } - self._test_guess_pageinfo(images_dir, expected) - - def test_guess_pageinfo_005(self): - """ - png だと正常に通るが jpg だと NG なケースについて、 - パラメータを修正して対応した。 - https://github.com/max747/fgojunks/issues/2 - """ - images_dir = get_images_absdir('005') - expected = { - '000.png': (2, 2, 4), - '000.jpg': (2, 2, 4), - '001.png': (2, 2, 4), - '001.jpg': (2, 2, 4), - '002.jpg': (1, 2, 4), - '003.png': (1, 2, 4), - '003.jpg': (1, 2, 4), - '004.png': (1, 2, 4), - '004.jpg': (1, 2, 4), - } - self._test_guess_pageinfo(images_dir, expected) diff --git a/shrinker/Pipfile b/shrinker/Pipfile new file mode 100644 index 0000000..a9452d4 --- /dev/null +++ b/shrinker/Pipfile @@ -0,0 +1,12 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] + +[packages] +pillow = "*" + +[requires] +python_version = "3.8" diff --git a/shrinker/Pipfile.lock b/shrinker/Pipfile.lock new file mode 100644 index 0000000..c4029bd --- /dev/null +++ b/shrinker/Pipfile.lock @@ -0,0 +1,88 @@ +{ + "_meta": { + "hash": { + "sha256": "67a9257d6a9f0bc0fdf74c6eb2d0ac2b94a5705f2152bd05a79b52e47d67275d" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.8" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "pillow": { + "hashes": [ + "sha256:03150abd92771742d4a8cd6f2fa6246d847dcd2e332a18d0c15cc75bf6703040", + "sha256:073adb2ae23431d3b9bcbcff3fe698b62ed47211d0716b067385538a1b0f28b8", + "sha256:0b07fffc13f474264c336298d1b4ce01d9c5a011415b79d4ee5527bb69ae6f65", + "sha256:0b7257127d646ff8676ec8a15520013a698d1fdc48bc2a79ba4e53df792526f2", + "sha256:12ce4932caf2ddf3e41d17fc9c02d67126935a44b86df6a206cf0d7161548627", + "sha256:15c42fb9dea42465dfd902fb0ecf584b8848ceb28b41ee2b58f866411be33f07", + "sha256:18498994b29e1cf86d505edcb7edbe814d133d2232d256db8c7a8ceb34d18cef", + "sha256:1c7c8ae3864846fc95f4611c78129301e203aaa2af813b703c55d10cc1628535", + "sha256:22b012ea2d065fd163ca096f4e37e47cd8b59cf4b0fd47bfca6abb93df70b34c", + "sha256:276a5ca930c913f714e372b2591a22c4bd3b81a418c0f6635ba832daec1cbcfc", + "sha256:2e0918e03aa0c72ea56edbb00d4d664294815aa11291a11504a377ea018330d3", + "sha256:3033fbe1feb1b59394615a1cafaee85e49d01b51d54de0cbf6aa8e64182518a1", + "sha256:3168434d303babf495d4ba58fc22d6604f6e2afb97adc6a423e917dab828939c", + "sha256:32a44128c4bdca7f31de5be641187367fe2a450ad83b833ef78910397db491aa", + "sha256:3dd6caf940756101205dffc5367babf288a30043d35f80936f9bfb37f8355b32", + "sha256:40e1ce476a7804b0fb74bcfa80b0a2206ea6a882938eaba917f7a0f004b42502", + "sha256:41e0051336807468be450d52b8edd12ac60bebaa97fe10c8b660f116e50b30e4", + "sha256:4390e9ce199fc1951fcfa65795f239a8a4944117b5935a9317fb320e7767b40f", + "sha256:502526a2cbfa431d9fc2a079bdd9061a2397b842bb6bc4239bb176da00993812", + "sha256:51e0e543a33ed92db9f5ef69a0356e0b1a7a6b6a71b80df99f1d181ae5875636", + "sha256:57751894f6618fd4308ed8e0c36c333e2f5469744c34729a27532b3db106ee20", + "sha256:5d77adcd56a42d00cc1be30843d3426aa4e660cab4a61021dc84467123f7a00c", + "sha256:655a83b0058ba47c7c52e4e2df5ecf484c1b0b0349805896dd350cbc416bdd91", + "sha256:68943d632f1f9e3dce98908e873b3a090f6cba1cbb1b892a9e8d97c938871fbe", + "sha256:6c738585d7a9961d8c2821a1eb3dcb978d14e238be3d70f0a706f7fa9316946b", + "sha256:73bd195e43f3fadecfc50c682f5055ec32ee2c933243cafbfdec69ab1aa87cad", + "sha256:772a91fc0e03eaf922c63badeca75e91baa80fe2f5f87bdaed4280662aad25c9", + "sha256:77ec3e7be99629898c9a6d24a09de089fa5356ee408cdffffe62d67bb75fdd72", + "sha256:7db8b751ad307d7cf238f02101e8e36a128a6cb199326e867d1398067381bff4", + "sha256:801ec82e4188e935c7f5e22e006d01611d6b41661bba9fe45b60e7ac1a8f84de", + "sha256:82409ffe29d70fd733ff3c1025a602abb3e67405d41b9403b00b01debc4c9a29", + "sha256:828989c45c245518065a110434246c44a56a8b2b2f6347d1409c787e6e4651ee", + "sha256:829f97c8e258593b9daa80638aee3789b7df9da5cf1336035016d76f03b8860c", + "sha256:871b72c3643e516db4ecf20efe735deb27fe30ca17800e661d769faab45a18d7", + "sha256:89dca0ce00a2b49024df6325925555d406b14aa3efc2f752dbb5940c52c56b11", + "sha256:90fb88843d3902fe7c9586d439d1e8c05258f41da473952aa8b328d8b907498c", + "sha256:97aabc5c50312afa5e0a2b07c17d4ac5e865b250986f8afe2b02d772567a380c", + "sha256:9aaa107275d8527e9d6e7670b64aabaaa36e5b6bd71a1015ddd21da0d4e06448", + "sha256:9f47eabcd2ded7698106b05c2c338672d16a6f2a485e74481f524e2a23c2794b", + "sha256:a0a06a052c5f37b4ed81c613a455a81f9a3a69429b4fd7bb913c3fa98abefc20", + "sha256:ab388aaa3f6ce52ac1cb8e122c4bd46657c15905904b3120a6248b5b8b0bc228", + "sha256:ad58d27a5b0262c0c19b47d54c5802db9b34d38bbf886665b626aff83c74bacd", + "sha256:ae5331c23ce118c53b172fa64a4c037eb83c9165aba3a7ba9ddd3ec9fa64a699", + "sha256:af0372acb5d3598f36ec0914deed2a63f6bcdb7b606da04dc19a88d31bf0c05b", + "sha256:afa4107d1b306cdf8953edde0534562607fe8811b6c4d9a486298ad31de733b2", + "sha256:b03ae6f1a1878233ac620c98f3459f79fd77c7e3c2b20d460284e1fb370557d4", + "sha256:b0915e734b33a474d76c28e07292f196cdf2a590a0d25bcc06e64e545f2d146c", + "sha256:b4012d06c846dc2b80651b120e2cdd787b013deb39c09f407727ba90015c684f", + "sha256:b472b5ea442148d1c3e2209f20f1e0bb0eb556538690fa70b5e1f79fa0ba8dc2", + "sha256:b59430236b8e58840a0dfb4099a0e8717ffb779c952426a69ae435ca1f57210c", + "sha256:b90f7616ea170e92820775ed47e136208e04c967271c9ef615b6fbd08d9af0e3", + "sha256:b9a65733d103311331875c1dca05cb4606997fd33d6acfed695b1232ba1df193", + "sha256:bac18ab8d2d1e6b4ce25e3424f709aceef668347db8637c2296bcf41acb7cf48", + "sha256:bca31dd6014cb8b0b2db1e46081b0ca7d936f856da3b39744aef499db5d84d02", + "sha256:be55f8457cd1eac957af0c3f5ece7bc3f033f89b114ef30f710882717670b2a8", + "sha256:c7025dce65566eb6e89f56c9509d4f628fddcedb131d9465cacd3d8bac337e7e", + "sha256:c935a22a557a560108d780f9a0fc426dd7459940dc54faa49d83249c8d3e760f", + "sha256:dbb8e7f2abee51cef77673be97760abff1674ed32847ce04b4af90f610144c7b", + "sha256:e6ea6b856a74d560d9326c0f5895ef8050126acfdc7ca08ad703eb0081e82b74", + "sha256:ebf2029c1f464c59b8bdbe5143c79fa2045a581ac53679733d3a91d400ff9efb", + "sha256:f1ff2ee69f10f13a9596480335f406dd1f70c3650349e2be67ca3139280cade0" + ], + "index": "pypi", + "version": "==9.3.0" + } + }, + "develop": {} +} diff --git a/shrinker/shrinker.py b/shrinker/shrinker.py new file mode 100755 index 0000000..6fce3f5 --- /dev/null +++ b/shrinker/shrinker.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 + +import argparse +import logging +import io +from pathlib import Path + +from PIL import Image + +logger = logging.getLogger(__name__) + +DEFAULT_QUALITY = 90 + + +def convert( + f: io.FileIO, + prefix: str = '', + quality: int = DEFAULT_QUALITY, + shrink_ratio: int = 100, + dryrun: bool = False, + ) -> None: + + logger.info(f.name) + im = Image.open(f) + logger.debug('%s %s %s', im.format, im.mode, im.size) + + if im.format == 'JPEG': + logger.warning('skip converting JPEG image: %s', f.name) + return + + p = Path(f.name) + jpeg_path = p.with_suffix('.jpg') + if prefix: + jpeg_path = jpeg_path.parent / f'{prefix}_{jpeg_path.name}' + + logger.info(' -> %s', jpeg_path) + + if dryrun: + return + + im_rgb = im.convert('RGB') + if shrink_ratio < 100: + w = im.width * shrink_ratio // 100 + h = im.height * shrink_ratio // 100 + logger.debug('resize: %s -> %s', im.size, (w, h)) + im_rgb = im_rgb.resize((w, h), Image.BICUBIC) + im_rgb.save(jpeg_path, 'JPEG', quality=quality) + + +def main(args: argparse.Namespace) -> None: + for f in args.file: + convert(f, args.prefix, args.quality, args.shrink_ratio, args.dry_run) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument('file', nargs='+', type=argparse.FileType('rb')) + parser.add_argument('-p', '--prefix') + parser.add_argument('-q', '--quality', type=int, default=DEFAULT_QUALITY) + parser.add_argument('-s', '--shrink-ratio', type=int, default=100) + parser.add_argument('--dry-run', action='store_true') + parser.add_argument('-l', '--loglevel', choices=('debug', 'info'), default='info') + return parser.parse_args() + + +if __name__ == '__main__': + args = parse_args() + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s [%(levelname)s] %(message)s', + ) + logger.setLevel(args.loglevel.upper()) + main(args)