calibre
Author: Kovid Goyal
Date: 2026-04-10T03:11:05+00:00
Description: Comprehensive documentation for calibre

탐색

- 모듈
- 다음 |
- 시작 »
- 캘리버(Calibre) 사용자 설명서

캘리버(Calibre) 사용자 설명서

캘리버는 전자책 라이브러리 관리자입니다. 대부분의 주요 전자책 형식의 전자책을 보고, 변환하고, 범주화할 수 있습니다. 또한 많은 전자책 리더 장치와 통신할 수 있습니다. 그것은 인터넷을 통해 당신의 책에 대한 메타데이터를 가져올 수 있습니다. 신문을 다운로드하여 전자책으로 변환하여 편리하게 읽을 수 있습니다. Linux, Windows 및 macOS에서 실행되는 크로스 플랫폼입니다.

당신은 이제 막 캘리버를 시작했습니다. 지금 무엇을 합니까? 캘리버가 전자책으로 무엇이든 할 수 있으려면 먼저 전자책에 대해 알아야 합니다. 몇 가지 전자책 파일을 캘리버로 끌어다 놓거나 “책 추가하기” 버튼을 클릭하고 작업하려는 전자책을 찾아보십시오. 책을 추가하면 기본 보기에 다음과 같이 표시됩니다.

여러분이 방금 마음껏 추가한 책 목록에 감탄했다면, 여러분은 아마 책을 읽고 싶을 것입니다. 그렇게 하려면 책을 리더가 이해할 수 있는 형식으로 변환해야 합니다. 캘리버를 처음 실행할 때 :guilabel:`시작 마법사`가 시작되고 리더 장치에 대한 캘리버를 설정합니다. 변환은 간단합니다. 변환하려는 책을 선택한 다음 “책 변환” 버튼을 클릭하기만 하면 됩니다. 지금은 모든 옵션을 무시하고 “확인”을 클릭하십시오. 오른쪽 하단 모서리에 있는 작은 아이콘이 회전하기 시작합니다. 회전이 끝나면 변환된 책이 준비된 것입니다. 책을 읽으려면 “보기” 버튼을 클릭합니다.

리더에서 책을 읽으려면 리더를 컴퓨터에 연결하고 캘리버가 책을 감지할 때까지 기다린 다음(10-20초) “장치로 보내기” 버튼을 클릭합니다. 아이콘이 다시 회전하지 않으면 리더를 분리하고 멀리 읽으십시오! 이전 단계에서 책을 변환하지 않은 경우 캘리버는 리더 장치가 이해할 수 있는 형식으로 책을 자동 변환합니다.

더 많은 고급 사용법을 시작하려면, gui`를 읽어보세요. 더 강력하고 다양한 기능을 원한다면, :doc:`generated/ko/cli-index`를 보세요. :doc:`faq 목록에서도 유용한 것을 찾을 수도 있습니다..

질문이 더 있거나 다른 사용자와 캘리버에 대해 논의하거나 특정 사항에 대해 도움을 요청하려면 :website:`포럼 및 기타 도움말 리소스 사용 가능한 <help>`가 있습니다.

구간

- 그래픽 사용자 인터페이스
- 즐겨찾는 뉴스 웹사이트 추가
- 전자책 뷰어
- 전자책 변환
- 전자책 편집하는 중
- calibre 콘텐츠 서버
- 전자책 비교
- 전자책 메타데이터 편집
- 자주 묻는 질문(FAQ)
- 튜토리얼
- calibre:// URL 스키마
- 캘리버 사용자 정의
- 명령행 인터페이스
- 캘리버 개발 환경 설정
- 디지털 저작권 관리 (DRM)
- 용어집

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 그래픽 사용자 인터페이스

그래픽 사용자 인터페이스

그래픽 사용자 인터페이스 *(GUI)*에서는 라이브러리 관리와 전자책 형식 변환 기능 전체에 접근할 수 있습니다. calibre의 기본 작업 흐름은 먼저 하드 디스크에서 책을 라이브러리에 추가하는 것입니다. calibre는 책에서 메타데이터를 자동으로 읽어 내부 데이터베이스에 추가하려고 시도합니다. 책이 데이터베이스에 들어가면 형식 변환, 읽기 장치로 전송, 컴퓨터에서 보기, 메타데이터 편집 등 다양한 :ref:`actions`를 수행할 수 있습니다. 여기에는 표지, 설명, 태그 등 여러 정보를 수정하는 작업도 포함됩니다. calibre는 추가한 파일의 복사본을 생성하며, 원본 파일은 변경되지 않습니다.

인터페이스는 여러 영역으로 나뉩니다:

- 동작

- 환경설정

- 범주

- 검색 및 정렬

- 검색 인터페이스

- 검색 저장

- 모든 책의 전체 텍스트 검색

- 가상 라이브러리

- 책 임시 표시

- 파일 이름에서 메타데이터 추측중

- 책 상세정보

- 태그 탐색기

- 표지 격자 보기

- 책장 보기

- 표지 탐색기

- 저자, 시리즈 등에 메모 추가하기

- 빨리보기

- 작업

- 키보드 단축키

동작

동작 도구 모음은 자주 사용하는 작업에 대한 편리한 바로가기를 제공합니다. 버튼을 마우스 오른쪽 버튼으로 클릭하면 기본 동작의 다른 변형을 수행할 수 있습니다. 컴퓨터에 전자책 리더가 연결되어 있는지에 따라 동작 도구 모음의 모양이 약간 달라질 수 있습니다.

- 책 추가하기

- 책정보 편집하기

- 책 변환하기

- 보기

- 장치로 보내기

- 뉴스 가져오기

- 라이브러리

- 장치

- 디스크에 저장하기

- 연결/공유

- 책 지우기

책 추가하기

Add books 동작에는 버튼을 마우스 오른쪽 버튼으로 클릭하면 접근할 수 있는 여러 변형이 있습니다.

1. 단일 폴더에서 책 추가: 파일 선택 대화 상자를 열어 폴더 안의 어떤 책을 추가할지 지정할 수 있습니다. 이 동작은 *문맥 의존적*입니다. 즉, 현재 어떤 :ref:`catalog <catalogs>`를 선택했는지에 따라 달라집니다. :guilabel:`Library`를 선택한 경우 책이 라이브러리에 추가됩니다. 전자책 리더 장치를 선택한 경우에는 책이 해당 장치로 업로드됩니다.

2. 폴더와 하위 폴더에서 책 추가: 폴더를 선택할 수 있습니다. 해당 폴더와 모든 하위 폴더를 재귀적으로 검색하여 발견된 전자책을 라이브러리에 추가합니다. 한 폴더에 있는 모든 파일을 하나의 책 레코드로 추가할지, 여러 책 레코드로 추가할지 선택할 수 있습니다. calibre는 각 폴더에 하나의 책이 들어 있다고 가정합니다. 폴더 안의 모든 전자책 파일은 서로 다른 형식의 같은 책으로 간주됩니다. 이 동작은 Save to disk 동작의 반대입니다. 즉, :guilabel:`Save to disk`로 저장한 뒤 책을 삭제하고 폴더당 한 권 모드로 다시 추가해도 날짜를 제외한 정보는 잃지 않습니다. 단, 디스크 저장 동작의 설정을 변경하지 않았다고 가정합니다.

3. 압축 파일(ZIP/RAR/7z)에서 여러 책 추가: 선택한 압축 파일 안에 들어 있는 여러 전자책을 추가할 수 있습니다. 먼저 압축을 푼 다음 위의 두 옵션 중 하나로 책을 추가할 필요가 없어서 편리한 바로가기입니다.

4. 빈 책 추가(형식이 없는 책 항목): 비어 있는 책 레코드를 만들 수 있습니다. 컬렉션에는 아직 없지만 정보를 직접 입력해 두고 싶은 책에 사용할 수 있습니다.

5. ISBN으로 추가: ISBN을 입력하여 한 권 이상의 책을 추가할 수 있습니다.

6. 선택한 책 레코드에 파일 추가: 라이브러리에 이미 있는 책과 연결된 파일을 추가하거나 업데이트할 수 있습니다.

7. 클립보드에서 선택한 책 레코드에 파일 추가: 현재 클립보드의 내용을 이용해 라이브러리에 이미 있는 책과 연결된 파일을 추가하거나 업데이트할 수 있습니다.

8. 선택한 책 레코드에 데이터 파일 추가: 책 폴더의 data 하위 디렉터리에 저장될 추가 파일을 원하는 만큼 넣을 수 있습니다. 자세한 내용은 :ref:`data_files`를 참조하십시오.

9. 선택한 책 레코드에 빈 파일 추가: 지정한 형식의 빈 파일을 선택한 책 레코드에 추가할 수 있습니다.

Add books 동작은 매우 다양한 전자책 형식에서 메타데이터를 읽을 수 있습니다. 또한 파일 이름에서 메타데이터를 추측하려고 시도합니다. 이를 설정하는 방법은 파일 이름에서 메타데이터 추측중 절을 참조하십시오.

기존 책에 추가 형식을 넣으려면 다음 세 가지 방법 중 하나를 사용할 수 있습니다:

1. 파일을 메인 창 오른쪽의 책 상세정보 패널로 끌어다 놓습니다

2. Add books 버튼을 마우스 오른쪽 버튼으로 클릭하고 :guilabel:`Add files to selected books`를 선택합니다.

3. 책정보 편집하기 동작으로 열 수 있는 Edit metadata 대화 상자 오른쪽 위 영역의 Add books 버튼을 클릭합니다.

책정보 편집하기

Edit metadata 동작에는 버튼을 마우스 오른쪽 버튼으로 클릭하면 접근할 수 있는 여러 변형이 있습니다.

1. 개별적으로 메타데이터 편집: 책의 메타데이터를 한 권씩 편집할 수 있으며, 표지를 포함한 메타데이터를 인터넷에서 가져오는 옵션도 제공합니다. 또한 특정 전자책 형식을 책에서 추가하거나 제거할 수 있습니다.

2. 일괄 메타데이터 편집: 많은 책의 공통 메타데이터 필드를 한 번에 편집할 수 있습니다. :ref:`Library view <search_sort>`에서 선택한 모든 책에 적용됩니다.

3. 메타데이터와 표지 다운로드: 책 목록에서 선택한 책의 메타데이터와 표지(있는 경우)를 다운로드합니다.

4. 메타데이터 복사: 현재 책의 메타데이터를 시스템 클립보드로 복사합니다

5. 메타데이터 붙여넣기: 시스템 클립보드의 메타데이터를 현재 선택한 책에 붙여넣습니다

6. 책 레코드 병합: 둘 이상의 책 레코드의 메타데이터와 형식을 병합할 수 있습니다. 처음 클릭하지 않은 레코드를 삭제할지 유지할지 선택할 수 있습니다.

7. 데이터 파일 관리: 선택한 책과 연결된 추가 데이터 파일을 관리합니다.

자세한 내용은 :ref:`metadata`를 참조하십시오.

책 변환하기

전자책은 여러 형식에서 전자책 리더가 선호하는 형식으로 변환할 수 있습니다. 구매 가능한 많은 전자책은 Digital Rights Management (DRM) 기술로 보호됩니다. calibre는 이러한 전자책을 변환하지 않습니다. 많은 형식에서 DRM을 제거하는 것은 어렵지 않지만, 불법일 수 있으므로 책을 해방하는 도구는 사용자가 직접 찾아야 하며 그 다음 calibre로 변환해야 합니다.

대부분의 사용자에게 변환은 버튼 한 번으로 끝나는 간단한 작업이어야 합니다. 변환 과정에 대해 더 알고 싶다면 :ref:`conversion`을 참조하십시오.

Convert books 동작에는 버튼을 마우스 오른쪽 버튼으로 클릭하면 접근할 수 있는 세 가지 변형이 있습니다.

1. 개별 변환: 선택한 각 전자책에 대해 변환 옵션을 지정하여 변환 내용을 사용자 지정할 수 있습니다.

2. 일괄 변환: 여러 전자책을 한꺼번에 변환할 때 옵션을 한 번만 지정할 수 있습니다.

3. calibre 라이브러리의 책 카탈로그 만들기: XML, CSV, BiBTeX, EPUB, MOBI 등 여러 형식으로 라이브러리에 있는 책의 전체 목록과 모든 메타데이터를 생성할 수 있습니다. 카탈로그에는 현재 라이브러리 보기에서 표시되는 모든 책이 들어갑니다. 따라서 검색 기능으로 카탈로그에 포함할 책을 제한할 수 있습니다. 또한 마우스로 여러 책을 선택했다면 그 책들만 카탈로그에 추가됩니다. EPUB, MOBI, AZW3 같은 전자책 형식으로 카탈로그를 만들면 다음에 전자책 리더를 연결할 때 카탈로그가 자동으로 장치로 전송됩니다. 카탈로그 동작 방식에 대한 자세한 내용은 :ref:`catalog_tut`를 읽어 보십시오.

보기

View 동작은 책을 전자책 뷰어 프로그램으로 표시합니다. calibre에는 여러 전자책 형식을 위한 내장 뷰어가 있습니다. 다른 형식은 운영 체제의 기본 응용 프로그램을 사용합니다. 어떤 형식을 내부 뷰어로 열지 :guilabel:`Preferences->Interface->Behavior`에서 설정할 수 있습니다. 한 책에 형식이 둘 이상 있다면 버튼을 마우스 오른쪽 버튼으로 클릭하여 특정 형식만 볼 수 있습니다.

장치로 보내기

Send to device 동작에는 버튼을 마우스 오른쪽 버튼으로 클릭하면 접근할 수 있는 여덟 가지 변형이 있습니다.

1. 주 메모리로 보내기: 선택한 책을 전자책 리더의 주 메모리로 전송합니다.

2. 카드(A)로 보내기: 선택한 책을 전자책 리더의 저장 카드(A)로 전송합니다.

3. 카드(B)로 보내기: 선택한 책을 전자책 리더의 저장 카드(B)로 전송합니다.

4. 특정 형식을 다음으로 보내기: 선택한 책을 사용자가 지정한 형식으로 장치의 선택한 저장 위치에 전송합니다.

5. 장치 꺼내기: calibre에서 장치를 분리합니다.

6. 기본 장치 전송 동작 설정: 위의 1~5번 또는 아래 7번 옵션 가운데 어떤 것을 메인 버튼 클릭 시 기본 동작으로 사용할지 지정할 수 있습니다.

7. 보내고 라이브러리에서 삭제: 선택한 책을 장치의 선택한 저장 위치로 전송한 다음 라이브러리에서 **삭제**합니다.

8. 주석 가져오기(실험적): 장치의 전자책에 작성한 주석을 calibre 라이브러리에 있는 해당 책의 댓글 메타데이터로 전송합니다.

:guilabel:`Preferences->Import/export->Sending books to devices`에서 템플릿을 설정하면 장치로 보내는 파일의 파일 이름과 폴더 구조를 제어할 수 있습니다. :ref:`templatelangcalibre`도 참조하십시오.

뉴스 가져오기

Fetch news 동작은 다양한 웹사이트에서 뉴스를 다운로드하여 전자책 리더에서 읽을 수 있는 전자책으로 변환합니다. 일반적으로 새로 생성된 전자책은 전자책 라이브러리에 추가되지만, 다운로드가 끝날 때 전자책 리더가 연결되어 있으면 뉴스도 자동으로 리더에 업로드됩니다.

Fetch news 동작은 각 뉴스 사이트마다 단순한 레시피(코드 10~15줄)를 사용합니다. 자신만의 뉴스 소스용 레시피를 만드는 방법은 :ref:`news`를 참조하십시오.

Fetch news 동작에는 버튼을 마우스 오른쪽 버튼으로 클릭하면 접근할 수 있는 세 가지 변형이 있습니다.

1. 뉴스 다운로드 예약: 수백 개의 뉴스 소스 목록 중에서 선택한 소스의 다운로드 일정을 예약할 수 있습니다. 각 뉴스 소스마다 개별적으로 일정을 설정할 수 있으며, 요일 지정이나 다운로드 간격 일수 지정 등 유연하게 설정할 수 있습니다.

2. 사용자 지정 뉴스 소스 추가: 원하는 사용자 지정 뉴스 사이트에서 뉴스를 다운로드하는 간단한 레시피를 만들 수 있습니다. RSS 뉴스 피드 URL만 지정해도 되고, 필요하면 Python 기반 코드를 작성해 더 세밀하게 제어할 수도 있습니다. 자세한 내용은 :ref:`news`를 참조하십시오.

3. 예약된 모든 뉴스 소스 다운로드: 예약해 둔 모든 뉴스 소스의 다운로드를 calibre가 즉시 시작합니다.

라이브러리

Library 동작은 라이브러리를 만들고, 전환하고, 이름을 바꾸고, 제거할 수 있게 해 줍니다. calibre에서는 원하는 만큼 라이브러리를 만들 수 있습니다. 예를 들어 소설 라이브러리, 비소설 라이브러리, 외국어 라이브러리, 프로젝트 라이브러리 등 필요에 맞는 구조를 만들 수 있습니다. 라이브러리는 calibre 안에서 가장 높은 수준의 정리 단위입니다. 각 라이브러리에는 자체 책 집합, 태그, 카테고리, 기본 저장 위치가 있습니다.

1. 라이브러리 전환/만들기…: a) 다른 위치에 있는 기존 calibre 라이브러리에 연결하거나, b) 새 위치에 빈 라이브러리를 만들거나, c) 현재 라이브러리를 새로 지정한 위치로 이동할 수 있습니다.

2. 빠른 전환: calibre 안에 등록되었거나 생성된 라이브러리 사이를 빠르게 전환할 수 있습니다.

3. 라이브러리 이름 바꾸기: 라이브러리 이름을 바꿀 수 있습니다.

4. 임의의 책 고르기: 라이브러리에서 임의의 책 한 권을 선택합니다

5. 라이브러리 제거: calibre에서 라이브러리 등록을 해제할 수 있습니다.

6. 모든 캘리버 데이터 내보내기/가져오기: 새 컴퓨터로 마이그레이션하기 위해 캘리버 데이터를 내보내거나 이전에 내보낸 데이터를 가져올 수 있습니다.

7. <library name>: 7, 8 등 이후 항목은 만들어 두었거나 연결해 둔 여러 라이브러리 사이를 즉시 전환할 수 있게 해 줍니다. 이 목록에는 가장 자주 사용하는 라이브러리 5개만 표시됩니다. 전체 목록은 Quick Switch 메뉴를 사용하십시오.

8. 라이브러리 유지 관리: 현재 라이브러리의 데이터 일관성 문제를 검사하고 백업에서 현재 라이브러리의 데이터베이스를 복원할 수 있습니다.

참고

전자책의 메타데이터(예: 제목, 저자, 태그)는 calibre 라이브러리 폴더 안의 metadata.db라는 단일 파일에 저장됩니다. 이 파일이 손상되면(매우 드문 일입니다) 메타데이터를 잃을 수 있습니다. 다행히 calibre는 각 책의 폴더 안에 OPF 파일로 각 책의 메타데이터를 자동 백업합니다. 위에서 설명한 Library Maintenance 아래의 Restore database 동작을 사용하면 calibre가 개별 OPF 파일에서 metadata.db 파일을 다시 만들 수 있습니다.

책을 마우스 오른쪽 버튼으로 클릭한 뒤 Copy to library 동작을 선택하면 서로 다른 라이브러리 사이에서 책을 복사하거나 이동할 수 있습니다(라이브러리를 두 개 이상 설정한 경우).

장치

Device 동작은 장치의 주 메모리나 저장 카드에 있는 책을 보거나 장치를 꺼내기(calibre에서 분리하기) 할 수 있게 해 줍니다. 지원되는 장치를 연결하면 이 아이콘이 자동으로 calibre 메인 도구 모음에 표시됩니다. 아이콘을 클릭하면 장치의 책을 볼 수 있습니다. calibre 라이브러리에서 이 아이콘으로 책을 끌어다 놓아 장치로 전송할 수도 있습니다. 반대로 장치에서 도구 모음의 라이브러리 아이콘으로 책을 끌어다 놓으면 장치의 책을 calibre 라이브러리로 가져올 수 있습니다.

디스크에 저장하기

Save to disk 동작에는 버튼을 마우스 오른쪽 버튼으로 클릭하면 접근할 수 있는 다섯 가지 변형이 있습니다.

1. **디스크에 저장*: 선택한 책을 폴더로 구성된 디스크에 저장합니다. 폴더 구조는 다음과 같습니다:

Author_(sort)
Title
Book Files

:guilabel:`Preferences->Import/export->Saving books to disk`에서 템플릿을 설정하면 디스크에 저장하는 파일의 파일 이름과 폴더 구조를 제어할 수 있습니다. :ref:`templatelangcalibre`도 참조하십시오.

2. **단일 폴더에 디스크에 저장*: 선택한 책을 단일 폴더에 디스크에 저장합니다.

1번과 2번의 경우 선택한 각 책에 대해 사용 가능한 모든 형식과 메타데이터가 디스크에 저장됩니다. 메타데이터는 OPF 파일에 저장됩니다. 저장한 책은 Add books 동작을 사용해 정보를 잃지 않고 다시 라이브러리로 가져올 수 있습니다.

3. 디스크에 *<선호 형식>*만 저장: (1)과 같은 폴더 구조로 선택한 책을 디스크에 저장하지만, 사용자가 선호하는 전자책 형식만 저장합니다. 선호 형식은 :guilabel:`Preferences->Interface->Behaviour->Preferred output format`에서 설정할 수 있습니다

4. 단일 폴더에 *<선호 형식>*만 저장: 선택한 책을 하나의 폴더에 저장하지만, 사용자가 선호하는 전자책 형식만 저장합니다. 선호 형식은 :guilabel:`Preferences->Interface->Behaviour->Preferred output format`에서 설정할 수 있습니다

5. 단일 형식을 디스크에 저장…: (1)과 같은 폴더 구조로 선택한 책을 디스크에 저장하지만, 팝업 목록에서 선택한 형식만 저장합니다.

연결/공유

Connect/share 동작은 컴퓨터의 장치나 폴더에 수동으로 연결할 수 있게 해 줍니다. 또한 웹 브라우저나 이메일을 통해 calibre 라이브러리에 접근할 수 있도록 설정할 수 있습니다.

Connect/share 동작에는 버튼을 마우스 오른쪽 버튼으로 클릭하면 접근할 수 있는 네 가지 변형이 있습니다.

1. 폴더에 연결: 컴퓨터의 임의의 폴더를 장치처럼 연결하여 calibre가 장치에 대해 제공하는 기능을 그 폴더에도 사용할 수 있게 합니다. 장치가 calibre에서 직접 지원되지 않지만 USB 디스크로 접근 가능한 경우 유용합니다.

2. 콘텐츠 서버 시작: calibre의 내장 웹 서버를 시작합니다. 시작되면(원할 경우) 인터넷의 웹 브라우저를 통해 calibre 라이브러리에 접근할 수 있습니다. 웹 서버 접근 방식은 :guilabel:`Preferences->Sharing->Sharing over the net`에서 설정할 수 있습니다

3. 전자메일 기반 책 공유 설정: 이메일로 책과 뉴스 피드를 공유할 수 있게 합니다. 이 옵션에 이메일 주소를 설정하면 calibre가 입력한 이메일 주소로 뉴스 업데이트와 책 업데이트를 보냅니다. calibre의 이메일 전송 방식은 :guilabel:`Preferences->Sharing->Sharing books by email`에서 설정할 수 있습니다. 하나 이상의 이메일 주소를 설정하면 이 메뉴 항목은 구성된 이메일 주소로 책을 보내는 메뉴 항목으로 바뀝니다.

책 지우기

Remove books 동작은 책을 **영구적으로 삭제**하므로 주의해서 사용해야 합니다. 이 동작은 *문맥 의존적*입니다. 즉, 어떤 catalog <catalogs>`를 선택했는지에 따라 달라집니다. :guilabel:`Library`를 선택한 경우 책이 라이브러리에서 제거됩니다. 전자책 리더 장치를 선택한 경우에는 책이 장치에서 제거됩니다. 특정 책의 특정 형식만 제거하려면 :ref:`edit_meta_information 동작을 사용하십시오. Remove books에도 버튼을 마우스 오른쪽 버튼으로 클릭하여 접근할 수 있는 다섯 가지 변형이 있습니다.

1. 선택한 책 제거: 책 목록에서 선택한 모든 책을 영구적으로 제거할 수 있습니다.

2. 선택한 책에서 특정 형식의 파일 제거…: 책 목록에서 선택한 책에서 지정한 형식의 전자책 파일을 영구적으로 제거할 수 있습니다.

3. 선택한 책에서 지정한 형식을 제외한 모든 형식 제거…: 책 목록에서 선택한 책에서 지정한 형식을 제외한 모든 형식의 전자책 파일을 영구적으로 제거할 수 있습니다.

4. 선택한 책에서 모든 형식 제거: 책 목록에서 선택한 책의 모든 전자책 파일을 영구적으로 제거할 수 있습니다. 메타데이터만 남습니다.

5. 선택한 책에서 표지 제거: 책 목록에서 선택한 책의 표지 이미지 파일을 영구적으로 제거할 수 있습니다.

6. 장치에서 일치하는 책 제거: 책 목록에서 선택한 책과 일치하는 전자책 파일을 연결된 장치에서 제거할 수 있습니다.

7. 최근 삭제 복원: 책이나 형식 제거 작업을 되돌릴 수 있습니다.

참고

Remove books`를 사용해 calibre 라이브러리에서 책을 삭제하면 책 레코드는 삭제되지만, 실제 책 파일은 며칠 동안 휴지통 폴더에 임시 보관됩니다. :guilabel:`Remove books 버튼을 마우스 오른쪽 버튼으로 클릭한 다음 Restore recently deleted 책을 선택하면 삭제를 되돌릴 수 있습니다.

환경설정

Preferences 동작은 calibre의 여러 동작 방식을 바꿀 수 있게 해 줍니다. 버튼을 마우스 오른쪽 버튼으로 클릭하면 접근할 수 있는 네 가지 변형이 있습니다.

1. 환경설정: calibre의 여러 동작 방식을 바꿀 수 있습니다. 버튼을 클릭해도 이 동작을 수행합니다.

2. 시작 마법사 실행: 캘리버를 처음 시작할 때 나타나는 :guilabel:`시작 마법사`를 시작할 수 있습니다.

3. calibre 기능 향상 플러그인 가져오기: calibre용 플러그인을 보여 주는 새 창을 엽니다. 이 플러그인은 제3자가 calibre 기능을 확장하기 위해 개발한 것입니다.

4. 디버그 모드로 다시 시작: 프로그램 사용 중 발생하는 문제를 calibre 개발자가 해결하는 데 도움이 되는 디버그 모드를 활성화할 수 있습니다. 대부분의 사용자에게는 개발자가 요청한 경우가 아니면 이 기능을 끈 상태로 두는 것이 좋습니다.

범주

*카탈로그*는 책의 모음입니다. calibre는 서로 다른 두 종류의 카탈로그를 관리할 수 있습니다:

1. 라이브러리: 컴퓨터의 calibre 라이브러리에 저장된 책 모음입니다.

2. 장치: 전자책 리더에 저장된 책 모음입니다. 리더를 컴퓨터에 연결하면 사용할 수 있습니다.

책 추가, 삭제, 보기 등 많은 작업은 문맥 의존적입니다. 예를 들어 Device 카탈로그를 선택한 상태에서 View 버튼을 클릭하면 calibre는 장치의 파일을 열어 보여 줍니다. Library 카탈로그를 선택한 상태라면 calibre 라이브러리의 파일이 대신 열립니다.

검색 및 정렬

검색 및 정렬 영역에서는 책 컬렉션에 대해 여러 강력한 작업을 수행할 수 있습니다.

- 열 제목을 클릭하여 제목, 저자, 날짜, 평점 등으로 정렬할 수 있습니다. 하위 정렬, 즉 여러 열을 기준으로 정렬하는 것도 가능합니다. 예를 들어 제목 열을 클릭한 다음 저자 열을 클릭하면 먼저 저자 기준으로 정렬되고, 같은 저자의 항목들은 제목 기준으로 정렬됩니다.

- 검색 표시줄을 사용해 특정 책이나 책 집합을 검색할 수 있습니다. 아래에서 더 자세히 설명합니다.

- 목록에서 바꾸려는 항목을 선택한 다음 E 키를 누르면 메타데이터를 빠르고 편리하게 편집할 수 있습니다.

- 책 집합에 대해 :ref:`actions`를 수행할 수 있습니다. 여러 책을 선택하려면 다음 중 하나를 사용합니다:

- Ctrl 키를 누른 채 선택하려는 책을 클릭합니다.

- Shift 키를 누른 채 선택하려는 범위의 시작 책과 끝 책을 클릭합니다.

- 구성 대화창을 사용하여 표시할 필드를 구성할 수 있습니다.

- 여러 열을 기반으로 하는 복잡한 하위 정렬을 수행하려면 Preferences → Toolbars & menus`를 통해 도구 모음에 :guilabel:`Sort by 도구를 추가하십시오.

검색 인터페이스

검색 표시줄에 검색어를 입력하여 모든 책 메타데이터를 검색할 수 있습니다. 예를 들면 다음과 같습니다:

Asimov Foundation format:lrf

이 검색은 메타데이터에 ``Asimov``와 ``Foundation``이 들어 있고 LRF 형식으로 제공되는 라이브러리의 모든 책과 일치합니다. 몇 가지 예를 더 들면 다음과 같습니다:

author:Asimov and not series:Foundation
title:"The Ring" or "This book is about a ring"
format:epub publisher:feedbooks.com

검색 종류

검색 종류는 네 가지입니다. contains, equality, regular expression`(참조: `regular expressions [https://en.wikipedia.org/wiki/Regular_expression]), `character variant`입니다. 접두 문자를 사용해 검색 종류를 선택합니다.

‘포함’ 검색

접두 문자가 없는 검색은 contains 검색이며 기본적으로 대소문자를 구분하지 않습니다. 검색 문자열이 지정한 메타데이터 안의 어디에든 나타나면 일치합니다. Preferences / Searching`의 `Case sensitive searching 옵션을 체크하면 contains 검색을 대소문자 구분으로 만들 수 있습니다. Unaccented characters match accented characters and punctuation is ignored 옵션이 체크되어 있으면 한 문자가 그 변형 문자 전체와 일치하고(예: e`는 `Ã©, Ã¨, Ãª, `Ã«`와 일치), 모든 문장 부호와 공백은 무시됩니다. 예를 들어 이 옵션이 체크되어 있고 다음 두 책 제목이 있다고 가정하면:

1. Big, BothÃ©red, and Bad

2. Big Bummer

그러면 다음 검색 결과가 나옵니다:

- ``title:”er”``는 둘 다 일치합니다(e`가 `Ã©`와 `e 모두와 일치하기 때문입니다).

- ``title:”g “``는 공백이 무시되므로 둘 다 일치합니다.

- ``title:”g,”``는 쉼표가 무시되므로 둘 다 일치합니다.

- ``title:”gb”``는 1번 책에서는 `, `가 무시되고 2번 책에서는 공백이 무시되므로 둘 다 일치합니다.

- ``title:”g b”``는 쉼표와 공백이 무시되므로 둘 다 일치합니다.

- ``title:”db”``는 `and Bad`의 공백이 무시되므로 #1과 일치합니다.

- ``title:”,”``는 쉼표가 무시되므로 둘 다 일치합니다(실제로는 모든 책과 일치합니다).

Unaccented characters match … 옵션이 체크되어 있지 않으면 문자 변형, 문장 부호, 공백 모두가 중요한 차이로 취급됩니다.

Case sensitive searching`과 `Unaccented characters match accented characters and punctuation is ignored 검색 옵션은 둘 중 하나만 설정할 수 있습니다.

‘동등’ 검색

동등 검색은 검색 문자열 앞에 등호(=)를 붙여 표시합니다. 예를 들어 질의 ``tag:”=science”``는 `science`와는 일치하지만 `science fiction`이나 `hard science`와는 일치하지 않습니다. 문자 변형은 구분됩니다. `Ã©`는 `e`와 일치하지 않습니다.

계층형 항목(예: A.B.C)에는 동등 검색의 두 가지 변형이 사용됩니다. 하나는 계층 접두 검색이고 다른 하나는 계층 구성 요소 검색입니다. 첫 번째는 등호 뒤에 마침표 하나(=.)를 붙여 계층형 항목의 앞부분과 일치시킵니다. 두 번째는 마침표 두 개(=..)를 붙여 계층형 항목 내부의 이름 하나와 일치시킵니다. 값으로 태그 ``History.Military.WWII``를 사용할 때의 예는 다음과 같습니다:

- tags:"=.History" : 참입니다. ``History``는 이 태그의 접두어입니다.

- tags:"=.History.Military" : 참입니다. ``History.Military``는 이 태그의 접두어입니다.

- tags:"=.History.Military.WWII" : 참입니다. ``History.Military.WWII``는 이 태그의 접두어이며, 완전 일치이긴 하지만 부적절한 접두어까지 포함합니다.

- tags:"=.Military" : 거짓입니다. ``Military``는 이 태그의 접두어가 아닙니다.

- tags:"=.WWII" : 거짓입니다. ``WWII``는 이 태그의 접두어가 아닙니다.

- tags:"=..History" : 참입니다. 계층 안에 History 값이 있습니다.

- tags:"=..Military" : 참입니다. 계층 안에 Military 값이 있습니다.

- tags:"=..WWII" : 참입니다. 계층 안에 WWII 값이 있습니다.

- tags:"=..Military.WWII" : 거짓입니다. .. 검색은 단일 값만 찾습니다.

‘정규 표현식’ 검색

정규 표현식 검색은 검색 문자열 앞에 물결표(~)를 붙여 표시합니다. Python-compatible regular expression <https://docs.python.org/library/re.html>`__이라면 무엇이든 사용할 수 있습니다. 정규 표현식에서 특수 문자를 이스케이프하는 데 쓰는 역슬래시는 질의를 해석하는 동안 단일 역슬래시가 제거되므로 두 번 써야 합니다. 예를 들어 괄호 문자 자체를 찾으려면 `(``를 입력해야 하며, 또는 super-quotes`(아래 참조)를 사용할 수도 있습니다. 정규 표현식 검색은 식이 앵커로 고정되지 않는 한 `contains 검색입니다. 문자 변형은 구분되므로 ``~e``는 ``Ã©``와 일치하지 않습니다.

‘문자 변형’ 검색

문자 변형 검색은 검색 문자열 앞에 캐럿(^)을 붙여 표시합니다. 이 검색은 위의 contains 검색과 비슷하지만 다음과 같은 차이가 있습니다:

- 문자 대소문자는 항상 무시됩니다.

- 문자 변형은 항상 서로 일치합니다.

- 문장 부호와 공백은 항상 중요한 차이로 취급됩니다.

Unaccented characters match accented characters and punctuation is ignored`와 :guilabel:`Case sensitive searching 검색 옵션은 무시됩니다. 이 검색의 동작에는 영향을 주지 않습니다.

다음은 Unaccented characters match… 옵션이 체크되어 있다고 가정하고(위 참조) 같은 두 책 제목에 대해 이 검색을 contains 검색과 비교한 것입니다:

1. Big, BothÃ©red, and Bad

2. Big Bummer

그러면 다음 문자 변형 검색 결과가 나옵니다:

- ``title:”^er”``는 둘 다 일치합니다(e`가 `Ã©`와 `e 모두와 일치하기 때문입니다)

- ``title:”^g”``는 둘 다 일치합니다

- ``title:”^g “``는 공백이 의미가 있으므로 #2와 일치합니다

- ``title:”^g,”``는 쉼표가 의미가 있으므로 #1과 일치합니다

- ``title:”^gb”``는 공백과 쉼표가 의미가 있으므로 아무것도 일치하지 않습니다

- ``title:”^g b”``는 쉼표가 의미가 있으므로 #2와 일치합니다

- ``title:”^db”``는 아무것도 일치하지 않습니다

- ``title:”^,”``는 쉼표가 의미가 있으므로 #1과 일치합니다(모든 책이 아니라)

검색 식 구문
search expression`은 선택적으로 ``and` 및 or 연산자로 구분된 search term`들의 나열입니다. 두 검색어 사이에 연산자가 없으면 ``and``로 간주합니다. ``and` 연산자는 or``보다 우선순위가 높습니다. 예를 들어 ``a or b and c``는 ``a or (b and c)``와 같습니다. 괄호를 사용해 우선순위를 바꿀 수 있습니다. 예를 들어 ``(a or b) and c``는 ``or``가 먼저 평가되게 합니다. ``not 연산자를 사용하면 검색 식 평가 결과를 부정(반전)할 수 있습니다. 예를 들면 다음과 같습니다:

- ``not tag:foo``는 태그 ``foo``가 없는 모든 책을 찾습니다

- ``not (author:Asimov or author:Weber)``는 Asimov 또는 Weber가 쓴 책이 아닌 모든 책을 찾습니다.

위 예에서는 search term`의 예를 보여 줍니다. 기본적인 `search term`은 공백, 큰따옴표(`”), 역슬래시(``), 괄호(( ))를 제외한 문자들의 나열입니다. 선택적으로 열 이름 지정자를 앞에 붙일 수 있는데, 이는 열의 lookup name 뒤에 콜론(:)이 오는 형태입니다. 예를 들어 ``author:Asimov``입니다. 검색어에 공백이 들어가야 한다면 ``title:”The Ring”``처럼 전체 검색어를 큰따옴표로 감싸야 합니다. 검색어에 큰따옴표 자체가 들어가야 한다면 역슬래시로 `escape`해야 합니다. 예를 들어 `The “Ball” and The “Chain”`이라는 시리즈를 찾으려면 다음과 같이 사용합니다:

series:"The \"Ball\" and The \"Chain\"

실제 역슬래시가 필요하다면(특히 regular expression 검색에서 자주 발생합니다) 두 개를 사용하십시오(\\).

특히 regular expression 및 template 검색에서는 원하는 결과가 나오도록 이스케이프를 모두 맞추기가 어렵습니다. 이럴 때는 super-quote`인 `”””sequence of characters”””``를 사용하십시오. 슈퍼 따옴표 안의 문자는 변경 없이 그대로 사용되며, 이스케이프 처리는 수행되지 않습니다.

추가 정보

등호, 물결표, 캐럿으로 시작하는 문자열을 검색하려면 문자열 앞에 역슬래시를 붙이십시오.

검색 문자열에 괄호나 공백이 들어 있으면 큰따옴표(“)로 감싸십시오. 예를 들어 태그 ``Science Fiction``이 있는 책을 찾으려면 ``tag:”=science fiction”``로 검색해야 합니다. ``tag:=science fiction``로 검색하면 태그 ``science``가 있고 어떤 메타데이터에든 단어 ``fiction``이 들어 있는 모든 책을 찾게 됩니다.

검색 상자 왼쪽의 버튼을 클릭하면 열리는 :guilabel:`Advanced search dialog`를 사용해 고급 검색 질의를 쉽게 만들 수 있습니다.

검색 가능한 필드는 tag, title, author, publisher, series, series_index, rating, cover, comments, format, identifiers, date, pubdate, search, size, vl 및 사용자 정의 열입니다. 장치가 연결되어 있으면 calibre 라이브러리 보기에서 ondevice 필드도 사용할 수 있습니다. 사용자 정의 열의 검색 이름(실제로는 lookup name)을 찾으려면 라이브러리 보기에서 해당 열 머리글 위에 마우스를 올리십시오.

날짜

날짜 검색을 위한 구문은:

pubdate:>2000-1 Will find all books published after Jan, 2000
date:<=2000-1-3 Will find all books added to calibre before 3 Jan, 2000
pubdate:=2009 Will find all books published in 2009

날짜가 모호한 경우 비교에는 현재 로캘이 사용됩니다. 예를 들어 mm/dd/yyyy 로캘에서는 2/1/2009를 2009년 2월 1일로 해석합니다. dd/mm/yyyy 로캘에서는 2009년 1월 2일로 해석합니다. 사용할 수 있는 특별한 날짜 문자열도 있습니다. today 문자열은 그날의 날짜를 뜻합니다. yesterday``와 ``thismonth``(또는 현재 언어로 번역된 동등한 문자열)도 동작합니다. 또한 ``daysago 문자열(이 역시 번역됨)을 사용해 며칠 전 날짜와 비교할 수 있습니다. 예를 들면 다음과 같습니다:

date:>10daysago
date:<=45daysago

영어가 아닌 calibre를 사용할 때 번역된 문자열로 인한 문제를 피하기 위해 _today, _yesterday, _thismonth, _daysago 문자열은 항상 사용할 수 있습니다. 이들은 번역되지 않습니다.

관계 비교를 사용한 날짜 및 숫자 값 검색

날짜 및 숫자 필드는 관계 연산자 ``=``(같음), ``>``(보다 큼), ``>=``(보다 크거나 같음), ``<``(보다 작음)을 지원합니다. , ``<=``(보다 작거나 같음) 및 ``!=``(같지 않음). 등급 필드는 숫자로 간주됩니다. 예를 들어 ``평점:>=3``을 검색하면 평점이 3 이상인 모든 책을 찾습니다.

특정 크기의 형식을 가진 책은 다음과 같이 검색할 수 있습니다:

- ``size:>1.1M``은 1.1MB보다 큰 형식을 가진 책을 찾습니다

- ``size:<=1K``는 1KB 이하의 형식을 가진 책을 찾습니다

태그처럼 여러 값을 가질 수 있는 필드의 항목 개수는 # 문자를 사용한 뒤 숫자 필드와 같은 구문으로 검색할 수 있습니다. 예를 들어 태그가 4개보다 많은 책을 찾으려면 ``tags:#>4``를 사용합니다. 태그가 정확히 10개인 책을 찾으려면 ``tags:#=10``을 사용합니다.

시리즈 인덱스

시리즈 인덱스는 검색할 수 있습니다. 기본 시리즈의 검색 이름은 ``series_index``입니다. 사용자 정의 시리즈 열의 경우 열 검색 이름 뒤에 _index를 붙입니다. 예를 들어 ``#my_series``라는 사용자 정의 시리즈 열의 인덱스를 검색하려면 ``#my_series_index``라는 검색 이름을 사용합니다. 시리즈 인덱스는 숫자이므로 위에서 설명한 관계 연산자를 사용할 수 있습니다.

저장된 검색

특수 필드 ``search``는 :ref:`saved searches <saved_searches>`에 사용됩니다. “My spouse’s books”라는 이름으로 검색을 저장했다면 Search bar에 ``search:”My spouse’s books”``를 입력해 저장한 검색을 다시 사용할 수 있습니다. 검색 저장 방법은 아래에서 더 설명합니다.

가상 라이브러리

특수 필드 vl``은 가상 라이브러리에 있는 책을 검색하는 데 사용됩니다. 예를 들어 ``vl:Read``는 *Read* 가상 라이브러리에 있는 모든 책을 찾습니다. ``vl:Read and vl:"Science Fiction" 검색은 Read*와 *Science Fiction 가상 라이브러리에 모두 속한 책을 찾습니다. vl: 뒤의 값은 가상 라이브러리 이름이어야 합니다. 가상 라이브러리 이름에 공백이 있으면 큰따옴표로 감싸십시오.

필드에 값이 있는지 여부

필드에 값이 있는지 없는지는 “true”와 “false”를 사용해 검색할 수 있습니다. 예를 들면 다음과 같습니다:

- ``cover:false``는 표지가 없는 모든 책을 찾습니다

- ``series:true``는 시리즈에 속한 모든 책을 찾습니다

- ``series:false``는 시리즈에 속하지 않은 모든 책을 찾습니다

- ``comments:false``는 댓글이 비어 있는 모든 책을 찾습니다

- ``formats:false``는 책 파일이 전혀 없는 모든 책(빈 레코드)을 찾습니다

예/아니오 사용자 정의 열

예/아니오 사용자 정의 열에서 false, empty, blank``를 검색하면 해당 열의 값이 정의되지 않은 책을 모두 찾습니다. ``true``를 검색하면 값이 정의되지 않은 것이 아닌 모든 책을 찾습니다. ``yes 또는 checked``를 검색하면 그 열에 ``Yes``가 있는 책을 찾습니다. ``no 또는 unchecked``를 검색하면 그 열에 ``No``가 있는 책을 찾습니다. ``yes, no, blank, empty, checked, unchecked``라는 단어는 번역되므로 현재 언어의 단어 또는 영어 단어 둘 다 사용할 수 있습니다. ``true``와 ``false 및 특수 값 _yes, _no, ``_empty``는 번역되지 않습니다.

식별자

식별자(예: ISBN, DOI, LCCN 등)는 확장 구문을 사용합니다. 식별자는 type:value 형태이며, 예를 들어 ``isbn:123456789``입니다. 확장 구문을 사용하면 검색할 형식과 값을 독립적으로 지정할 수 있습니다. 질의의 형식 부분과 값 부분 모두 :ref:`search kinds <search_kinds>`에서 설명한 검색 종류를 사용할 수 있습니다. 예를 들면 다음과 같습니다:

- ``identifiers:true``는 아무 식별자나 하나 이상 있는 책을 찾습니다.

- ``identifiers:false``는 식별자가 전혀 없는 책을 찾습니다.

- ``identifiers:123``은 값에 `123`이 포함된 아무 형식의 식별자를 가진 책을 찾습니다.

- ``identifiers:=123456789``은 값이 `123456789`와 같은 아무 형식의 식별자를 가진 책을 찾습니다.

- identifiers:=isbn: 및 ``identifiers:isbn:true``는 형식이 ISBN이고 값은 무엇이든 있는 책을 찾습니다

- ``identifiers:=isbn:false``는 형식이 ISBN인 식별자가 없는 책을 찾습니다.

- ``identifiers:=isbn:123``은 형식이 ISBN이며 값에 `123`이 포함된 책을 찾습니다.

- ``identifiers:=isbn:=123456789``은 형식이 ISBN이며 값이 `123456789`와 같은 책을 찾습니다.

- ``identifiers:i:1``은 형식에 `i`가 포함되고 값에 `1`이 포함된 책을 찾습니다.

태그 탐색기에 표시되는 카테고리
in_tag_browser:true 검색은 현재 Tag browser`에 표시되고 있는 카테고리(태그, 저자 등)에 속한 모든 책을 찾습니다. 이 기능은 :guilabel:`Preferences → Look & feel → Tag browser → Hide empty categories`와 :guilabel:`Find shows all items that match 두 환경설정을 사용할 때 유용합니다. 이 둘이 설정되어 있으면 Tag browser`에서 :guilabel:`Find`를 수행했을 때 :guilabel:`Find`와 일치하는 항목을 포함하는 카테고리만 표시됩니다. 그 다음 ``in_tag_browser:true` 검색을 사용하면 이러한 카테고리/항목을 가진 책도 함께 찾을 수 있습니다. Ctrl+Alt+Shift+F 키를 누르거나 Tag browser`의 설정 버튼을 클릭한 뒤 :guilabel:`Show only books that have visible categories 항목을 선택해 이 검색을 쉽게 실행할 수 있습니다.

템플릿을 사용한 검색

:ref:`templatelangcalibre`의 템플릿을 메타데이터 필드 대신 사용하여 검색할 수 있습니다. 이를 위해 템플릿, 검색 유형, 검색 값을 입력합니다. 구문은 다음과 같습니다:

template: (the template) #@#: (search type) : (the value)

``template``은 유효한 calibre 템플릿 언어 템플릿이면 무엇이든 가능합니다. ``search type``은 ``t``(텍스트 검색), ``d``(날짜 검색), ``n``(숫자 검색), ``b``(설정/미설정(불리언)) 중 하나여야 합니다. ``value``는 원하는 값이면 무엇이든 가능하며, 각 검색 유형에 대해 위에서 설명한 :ref:`search kinds <search_kinds>`를 사용할 수 있습니다. 전체 검색 문자열 안에 공백이 하나라도 있으면 전체를 큰따옴표로 감싸야 합니다.

예:

- template:"program: connected_device_name('main')#@#:t:kindle" – kindle 장치가 연결되어 있을 때 참입니다.

- template:"program: select(formats_sizes(), 'EPUB')#@#:n:>1000000" – 1MB보다 큰 EPUB 파일을 가진 책을 찾습니다.

- template:"program: select(formats_modtimes('iso'), 'EPUB')#@#:d:>10daysago" – 10일 전보다 새 EPUB 파일을 가진 책을 찾습니다.

- template:"""program: book_count('tags:^"' & $series & '"', 0) != 0#@#:n:1""" – 태그 안에 시리즈 이름이 들어 있는 모든 책을 찾습니다. 이 예제는 템플릿이 검색 식을 만들 때 작은따옴표(')와 큰따옴표(")를 모두 사용하므로 슈퍼 따옴표를 사용합니다.

버튼을 클릭하면 열리는 :guilabel:`Advanced search dialog`를 사용해 템플릿 검색 질의를 쉽게 만들 수 있습니다. 특정 책에서 템플릿을 시험해 보려면 calibre의 :guilabel:`Template tester`를 사용할 수 있으며, 이는 :guilabel:`Preferences->Toolbars & menus`를 통해 도구 모음이나 메뉴에 추가할 수 있습니다. :guilabel:`Preferences->Shortcuts`에서 키보드 단축키를 지정할 수도 있습니다.

고급 검색 대화 상자

고급 검색 대화 상자

검색 저장
calibre에서는 자주 사용하는 검색을 특별한 이름으로 저장한 뒤 클릭 한 번으로 다시 사용할 수 있습니다. 이렇게 하려면 Search bar에 직접 검색을 입력하거나 Tag browser를 사용해 검색을 만든 다음, Search bar 옆 Saved Searches 상자에 해당 검색에 붙일 이름을 입력합니다. 그런 다음 저장된 검색 상자 옆의 더하기 아이콘을 클릭해 검색을 저장합니다.

이제 Tag browser의 Saved searches 아래에서 저장한 검색에 접근할 수 있습니다. 한 번 클릭하면 아무리 복잡한 검색이라도 다시 만들 필요 없이 쉽게 재사용할 수 있습니다.

모든 책의 전체 텍스트 검색
calibre는 라이브러리의 책 전체 텍스트를 선택적으로 색인하여 어떤 책 안에 있는 단어든 즉시 검색할 수 있게 해 줍니다. 이 기능을 사용하려면 검색 표시줄 왼쪽 끝의 FT 버튼을 클릭하십시오.

그런 다음 현재 라이브러리에 대해 색인을 활성화하십시오. 색인이 완료되면 라이브러리 전체 텍스트를 검색할 수 있습니다. 라이브러리에 새 책을 추가하면 백그라운드에서 자동으로 색인됩니다. 이 검색을 사용하면 특정 단어나 단어 조합이 들어 있는 모든 책을 빠르게 찾을 수 있습니다. 검색 팝업 창의 예처럼 서로 가까이 나타나는 단어도 검색할 수 있습니다. 이 검색 도구는 특정 책에서 검색 질의가 나온 모든 위치를 나열하는 것이 아니라 한 번의 위치만 찾아낸다는 점에 유의하십시오. 모든 위치를 보려면 calibre의 :guilabel:`E-book viewer`에서 책 내부를 검색하는 것이 가장 좋습니다.

calibre에서 :guilabel:`Book details panel`을 마우스 오른쪽 버튼으로 클릭한 다음 :guilabel:`Re-index this book for full text searching`을 선택하면 개별 책을 다시 색인할 수 있습니다.

가상 라이브러리

:guilabel:`Virtual library`는 전체 라이브러리 대신 일부 책만 있는 것처럼 calibre 라이브러리를 가정하도록 만드는 기능입니다. 큰 책 컬렉션을 더 작고 관리하기 쉬운 덩어리로 나누는 훌륭한 방법입니다. 가상 라이브러리를 만들고 사용하는 방법은 튜토리얼 :ref:`virtual_libraries`를 참조하십시오.

책 임시 표시

임의의 책 집합을 임시로 표시할 수 있습니다. 표시된 책에는 핀이 붙으며 marked:true 검색으로 찾을 수 있습니다. 책을 표시하려면 Ctrl+M`을 누르거나 :guilabel:`Preferences->Toolbars & menus`로 가서 메인 도구 모음에 :guilabel:`Mark books 버튼을 추가하십시오.

Mark books 버튼을 마우스 오른쪽 버튼으로 클릭하고 Mark books with text label`을 선택하면 특정 텍스트 레이블로 책을 표시할 수 있습니다. 텍스트 레이블이 붙은 책은 나중에 ``marked:”=the-text-you-entered”` 검색으로 찾을 수 있습니다.

파일 이름에서 메타데이터 추측중

기본적으로 calibre는 책 파일 내부에서 메타데이터를 읽습니다. 그러나 :guilabel:`Preferences->Import/export->Adding books->Read metadata from file contents`를 통해 대신 파일 이름에서 메타데이터를 읽도록 설정할 수도 있습니다.

정규 표현식(참조: calibre에서 정규 표현식 사용하기)을 사용해 파일 이름에서 메타데이터를 읽는 방식도 제어할 수 있습니다. 설정 대화 상자의 Adding books 항목에서 calibre가 라이브러리에 추가하는 전자책 파일 이름으로부터 메타데이터를 추측하는 데 사용할 정규 표현식을 지정할 수 있습니다. 기본 정규 표현식은 다음과 같습니다:

title - author

즉, 첫 번째 - 앞까지의 모든 문자를 책 제목으로, 그 뒤의 문자를 저자로 가정합니다. 예를 들어 파일 이름이 다음과 같다면:

Foundation and Earth - Isaac Asimov.txt

제목은 Foundation and Earth, 저자는 Isaac Asimov로 해석됩니다

팁

파일 이름에 하이픈이 없으면 위 정규 표현식은 실패합니다.

책 상세정보

책 상세정보 표시는 현재 선택한 책의 표지와 모든 메타데이터를 보여 줍니다. calibre 메인 창 오른쪽 아래의 Layout 버튼을 통해 숨길 수 있습니다. 책 상세정보 패널에 표시된 저자 이름은 클릭할 수 있으며, 기본적으로 해당 저자의 Wikipedia 페이지로 이동합니다. 이 동작은 저자 이름을 마우스 오른쪽 버튼으로 클릭해 Manage this author를 선택하여 사용자 지정할 수 있습니다.

마찬가지로 책의 메타데이터를 다운로드하면, 책 상세정보 패널에는 해당 메타데이터를 가져온 Amazon, WorldCat 등의 책 웹 페이지로 연결되는 링크가 자동으로 표시됩니다.

책 상세정보 패널에서 개별 전자책 형식을 마우스 오른쪽 버튼으로 클릭하면 삭제, 원본과 비교, 디스크에 저장, 외부 프로그램으로 열기 등을 할 수 있습니다.

이미지를 책 상세정보 패널로 끌어다 놓기만 하면 책 표지를 바꿀 수 있습니다. 외부 프로그램에서 표지 이미지를 편집하고 싶다면 해당 이미지를 마우스 오른쪽 버튼으로 클릭하고 :guilabel:`Open with`를 선택하십시오.

파일을 책 상세정보 패널로 끌어다 놓아 현재 책에 전자책 파일을 추가할 수도 있습니다.

책 상세정보 패널을 두 번 클릭하면 별도의 팝업 창으로 열립니다.

마지막으로 :guilabel:`Preferences->Interface->Look & feel->Book details`를 통해 책 상세정보 패널에 정확히 어떤 정보를 표시할지 사용자 지정할 수 있습니다.

태그 탐색기

태그 탐색기를 사용하면 저자/태그/시리즈 등으로 컬렉션을 쉽게 둘러볼 수 있습니다. 태그 탐색기에서 어떤 항목이든 클릭하면, 예를 들어 저자 이름 Isaac Asimov를 클릭하면, 오른쪽의 책 목록은 그 저자의 책만 보여 주도록 제한됩니다. 카테고리 이름 자체도 클릭할 수 있습니다. 예를 들어 “Series”를 클릭하면 어떤 시리즈에 속한 책이든 모두 표시됩니다.

항목을 처음 클릭하면 그 항목을 포함하거나 일치하는 책만 목록에 남습니다. 위 예를 계속 사용하면 Isaac Asimov를 클릭하면 그 저자의 책이 표시됩니다. 같은 항목을 다시 클릭하면 해당 항목에 자식이 있는지에 따라 표시 내용이 바뀝니다(아래의 하위 카테고리와 계층형 항목 참조). Isaac Asimov 예를 계속 들면, 다시 클릭하면 Isaac Asimov가 아닌 책만 표시됩니다. 세 번째 클릭은 제한을 제거하여 모든 책을 다시 보여 줍니다. Ctrl 또는 Shift 키를 누른 채 여러 항목을 클릭하면 여러 항목을 기준으로 한 제한을 만들 수 있습니다. 예를 들어 유럽 역사 책을 찾기 위해 :kbd:`Ctrl`을 누른 채 History와 Europe 태그를 클릭할 수 있습니다. 태그 탐색기는 자동으로 Search bar에 입력되는 검색 식을 만들어 동작합니다. 태그 탐색기가 생성한 검색 식을 살펴보면 기본 검색 식 작성 방법을 익히는 데 도움이 됩니다.

태그 탐색기의 항목 아이콘은 일부가 색으로 채워져 있습니다. 채워지는 양은 해당 카테고리 책들의 평균 평점에 따라 달라집니다. 예를 들어 Isaac Asimov의 책 평균 평점이 별 네 개라면 태그 탐색기에서 Isaac Asimov의 아이콘은 5분의 4가 색칠되어 보입니다. 아이콘 위에 마우스를 올리면 평균 평점을 확인할 수 있습니다.

Tag browser`의 바깥쪽 수준 항목(예: Authors, Series 등)은 카테고리라고 부릅니다. 항목을 정리하는 데 유용한 :guilabel:`User categories`라는 사용자 카테고리를 직접 만들 수도 있습니다. 예를 들어 :guilabel:`User categories editor`(:guilabel:`Tag browser 왼쪽 아래의 Configure 버튼을 클릭한 다음 Manage authors, tags, etc → User categories`를 선택)를 사용해 ``Favorite Authors``라는 사용자 카테고리를 만든 뒤 즐겨찾는 저자를 그 안에 넣을 수 있습니다. 사용자 카테고리에는 하위 카테고리도 둘 수 있습니다. 예를 들어 사용자 카테고리 ``Favorites.Authors``는 ``Favorites``의 하위 카테고리입니다. ``Favorites.Series``도 만들 수 있으며, 이 경우 ``Favorites` 아래에 두 개의 하위 카테고리가 생깁니다. 하위 카테고리는 사용자 카테고리를 마우스 오른쪽 버튼으로 클릭하고 :guilabel:`Add sub-category to…`를 선택한 뒤 이름을 입력해서 만들 수 있습니다. 또는 위 Favorites 예와 같은 이름을 입력하여 :guilabel:`User categories editor`를 사용해 만들 수도 있습니다.

사용자 카테고리는 클릭하여 내장 카테고리와 같은 방식으로 검색할 수 있습니다. 클릭할 때 다음 네 가지 검색이 순환합니다:

1. 녹색 더하기 하나로 표시되는 “카테고리 안의 항목과 일치하는 모든 것”.

2. 녹색 더하기 두 개로 표시되는 “카테고리 또는 그 하위 카테고리의 항목과 일치하는 모든 것”.

3. 빨간 빼기 하나로 표시되는 “카테고리 안의 항목과 일치하지 않는 모든 것”.

4. 빨간 빼기 두 개로 표시되는 “카테고리 또는 그 하위 카테고리의 항목과 일치하지 않는 모든 것”.

태그, 시리즈, 사용자 정의 열 같은 일부 텍스트 카테고리 안에는 계층 구조를 만들 수도 있습니다. 이런 계층 구조는 작은 삼각형으로 표시되어 하위 항목을 숨길 수 있게 해 줍니다. 카테고리 안의 항목 계층 구조를 사용하려면 먼저 :guilabel:`Preferences->Interface->Look & feel`로 가서 “Categories with hierarchical items” 필드에 해당 카테고리 이름을 입력해야 합니다. 이렇게 하면 그 카테고리 안에서 마침표를 포함한 항목이 작은 삼각형과 함께 표시됩니다. 예를 들어 “Genre”라는 사용자 정의 열을 만들고 계층형 항목이 들어 있다고 지정했다고 가정합시다. 그러면 Mystery.Thriller, Mystery.English 같은 항목은 옆에 작은 삼각형이 붙은 Mystery로 표시됩니다. 삼각형을 클릭하면 Thriller와 English가 하위 항목으로 나타납니다. 자세한 내용은 :ref:`Managing subgroups of books, for example “genre” <subgroups-tutorial>`를 참조하십시오.

계층형 항목(자식이 있는 항목)은 사용자 카테고리와 같은 네 가지 클릭 검색을 사용합니다. 자식이 없는 항목은 “everything matching”과 “everything not matching” 두 가지 검색만 사용합니다.

태그 탐색기의 항목을 사용자 카테고리로 끌어다 놓아 그 카테고리에 추가할 수 있습니다. 원본이 사용자 카테고리인 경우, 끌 때 Shift 키를 누르고 있으면 새 카테고리로 이동됩니다. 책 목록의 책을 태그 탐색기 항목으로 끌어다 놓을 수도 있는데, 이렇게 하면 그 항목이 떨어뜨린 책들에 자동으로 적용됩니다. 예를 들어 책을 Isaac Asimov에 끌어다 놓으면 그 책의 저자가 Isaac Asimov로 설정됩니다. 책을 History 태그에 떨어뜨리면 해당 책의 태그에 History가 추가됩니다.

오른쪽 아래 모서리의 검색 버튼을 클릭하면 태그 탐색기에서 어떤 항목이든 쉽게 찾을 수 있습니다. 또한 항목을 마우스 오른쪽 버튼으로 클릭해 여러 작업 중 하나를 선택할 수 있습니다. 예를 들어 숨기기, 이름 바꾸기, 또는 그 종류의 항목을 관리할 수 있는 “Manage x” 대화 상자를 여는 작업 등이 있습니다. 예를 들어 Manage authors 대화 상자에서는 저자 이름을 바꾸고 정렬 방식을 제어할 수 있습니다.

태그 탐색기 왼쪽 아래의 Configure 버튼을 통해 항목 정렬 방식을 제어할 수 있습니다. 이름, 평균 평점, 인기도로 정렬할 수 있습니다(인기도는 라이브러리에서 해당 항목을 가진 책의 수입니다. 예를 들어 Isaac Asimov의 인기도는 라이브러리에 있는 Isaac Asimov 저서 수입니다).

카테고리와 카테고리 안의 값에 대해 자신만의 아이콘을 사용할 수 있습니다. 카테고리 아이콘을 바꾸려면 카테고리(바깥쪽 수준 항목)를 마우스 오른쪽 버튼으로 클릭한 뒤 `Change (category name) category icon`을 선택하십시오. 아이콘으로 사용할 이미지를 고르는 대화 상자가 열립니다. 기본 아이콘으로 되돌리려면 `Restore (category name) default icon`을 선택하십시오.

카테고리 안의 값에 대한 아이콘을 고르려면 값을 마우스 오른쪽 버튼으로 클릭한 뒤 `Manage icon for (value name)`을 선택하십시오. 다음과 같은 선택지가 표시됩니다:

- 이 값에만 아이콘 선택, 자식에는 적용하지 않기. 값에 대한 아이콘을 고르는 대화 상자가 열립니다. 이 값의 자식들은 그 아이콘을 상속받지 않습니다.

- 이 값과 그 자식에 아이콘 선택. 값에 대한 아이콘을 선택하는 대화 상자가 열립니다. 자체 아이콘이 지정되지 않은 자식은 이 아이콘을 상속받습니다.

- 이 값에는 기존 아이콘을 사용하되 자식에는 적용하지 않기. 이 옵션은 값에 이미 아이콘이 있고 그 아이콘이 자식에게 상속되고 있을 때 제공됩니다. 선택하면 아이콘이 값에는 적용되지만 자식에는 적용되지 않습니다.

- 이 값과 그 자식에 기존 아이콘 사용. 이 옵션은 값에 이미 아이콘이 있지만 그 아이콘이 자식에게 상속되지 않을 때 제공됩니다. 선택하면 아이콘이 값과 자식 모두에 적용됩니다.

- 이 값에 기본 아이콘 사용. 항목에 아이콘이 있을 때 제공되는 옵션입니다. 값에서 아이콘을 제거하고 그 아이콘을 상속받던 자식에서도 제거합니다. 기본 아이콘은 아래에서 지정한 것입니다.

- 모든 값 아이콘을 기본 아이콘으로 재설정. 이 옵션은 해당 카테고리의 모든 항목 값 아이콘을 제거합니다. 템플릿이 있는 경우 템플릿은 제거하지 않습니다. 실행 취소는 없습니다.

- 기본 값 아이콘을 고르는 템플릿 사용/편집. 이 옵션을 사용하면 아이콘 파일 이름을 반환하는 calibre 템플릿을 제공하여 기본 아이콘으로 사용할 수 있습니다. 템플릿은 다음 변수를 사용할 수 있습니다:

- category: 카테고리의 lookup 이름입니다. 예: authors, series, #mycolumn.

- value: 카테고리 안의 해당 항목 값입니다.

- count: 이 값을 가진 책의 수입니다. 값이 계층 구조의 일부라면 자식 수까지 포함합니다.

- avg_rating: 이 값을 가진 책의 평균 평점입니다. 값이 계층 구조의 일부라면 자식의 평균도 포함합니다.

제목 같은 책 메타데이터는 사용할 수 없습니다. ff_book_count`와 :ref:`ff_book_values 같은 템플릿 데이터베이스 함수는 동작하지만 성능이 만족스럽지 않을 수 있습니다. 다음 템플릿 함수는 GUI에서는 동작하지만 콘텐츠 서버에서는 동작하지 않습니다: connected_device_name, connected_device_uuid, current_virtual_library_name, is_marked, virtual_libraries.

GUI에서 Python 템플릿은 calibre 데이터베이스에 완전히 접근할 수 있습니다. 콘텐츠 서버에서는 Python 템플릿이 새 API(API documentation for the database interface [https://manual.calibre-ebook.com/db_api.html] 참조)에는 접근할 수 있지만, 이전 API(LibraryDatabase)에는 접근할 수 없습니다.

예를 들어 이 템플릿은 클릭한 카테고리에서 `History`로 시작하는 값이 있으면 ``flower.png``라는 아이콘을 사용하도록 지정합니다:

program:
if substr($value, 0, 7) == 'History' then 'flower.png' fi

템플릿이 빈 문자열('')을 반환하면 카테고리 아이콘이 사용됩니다. 존재하지 않는 파일 이름을 반환하면 아무 아이콘도 표시되지 않습니다.

- 카테고리 아이콘을 기본값으로 사용. 이 옵션은 별도의 아이콘이 없는 값에 대해 카테고리 아이콘을 사용하도록 지정합니다. 이 옵션을 선택하면 템플릿 아이콘 지정은 제거됩니다.

아이콘은 다음 계층에 따라 선택됩니다:

1. 값에 지정된 아이콘이 있으면 그것을 사용합니다.

2. 트리를 따라 위로 올라가며 찾은 부모 노드에 아이콘이 있으면 그것을 사용합니다.

3. 템플릿이 있고 비어 있지 않은 문자열을 반환하면 템플릿의 아이콘을 사용합니다.

4. 항상 존재하는 기본 카테고리 아이콘.

아이콘은 라이브러리별이 아니라 사용자별이며, calibre 설정 폴더에 저장됩니다. 항목 값 아이콘은 tb_icons 하위 폴더에 저장됩니다. 템플릿이 사용하는 아이콘은 tb_icons`의 :file:`template_icons 하위 폴더에 있습니다.

표지 격자 보기

목록 대신 책 표지 그리드로 컬렉션을 표시하도록 calibre를 설정할 수 있습니다. 표지로 컬렉션을 탐색하고 싶을 때 유용합니다. Cover grid`는 calibre 메인 창 오른쪽 아래의 :guilabel:`Layout 버튼을 클릭하면 활성화됩니다. :guilabel:`Preferences->Interface->Look & feel->Cover grid`에서 표지 크기와 :guilabel:`Cover grid`의 배경을 사용자 지정할 수 있습니다. 제목, 저자, 평점, 또는 사용자가 만든 사용자 정의 열 등 원하는 필드를 표지 아래에 표시하도록 할 수도 있습니다.

책장 보기

책 컬렉션을 선반 형태로 표시하도록 calibre를 설정할 수도 있으며, 필요하면 저자/날짜/평점 등으로 그룹화할 수 있습니다. Bookshelf view`는 calibre 메인 창 오른쪽 아래의 :guilabel:`Layout 버튼을 클릭하면 활성화됩니다. :guilabel:`Preferences->Interface->Look & feel->Bookshelf`에서 배경부터 책등의 글자, 글꼴 등에 이르기까지 보기의 모든 요소를 사용자 지정할 수 있습니다.

표지 탐색기

위에서 설명한 Cover grid 외에도 calibre는 표지를 한 줄로 표시할 수 있습니다. 이것은 메인 창 오른쪽 아래의 Layout 버튼으로 활성화합니다. Preferences → Interface → Look & feel → Cover browser`에서 표시할 표지 수를 바꿀 수 있으며, :guilabel:`Cover browser 자체를 별도의 팝업 창으로 표시할 수도 있습니다.

저자, 시리즈 등에 메모 추가하기

저자/시리즈/태그/출판사 등에 대한 메모를 calibre 라이브러리에 추가할 수 있습니다. 왼쪽의 Tag browser`나 오른쪽의 :guilabel:`Book details 패널에서 해당 이름을 마우스 오른쪽 버튼으로 클릭한 뒤 Create note 또는 :guilabel:`Edit note`를 선택하면 됩니다.

간단한 팝업 창이 열리며 기본 서식과 링크, 이미지 지원을 사용해 메모를 입력할 수 있습니다. 예를 들어 저자에 대한 메모를 만들면, 나중에 Book details 패널에서 저자 이름 옆의 작은 연필 아이콘을 클릭해 쉽게 볼 수 있습니다.

Ctrl+Shift+N`을 누르거나 :guilabel:`Preferences->Toolbars & menus`를 통해 도구 모음에 추가한 :guilabel:`Browse notes 도구를 사용해 라이브러리의 모든 메모를 검색할 수 있습니다.

빨리보기

현재 라이브러리 보기를 바꾸지 않으면서, 선택한 책과 같은 카테고리 값(저자, 태그, 출판사, 시리즈 등)을 가진 책 목록을 빠르게 보고 싶은 경우가 있습니다. 이럴 때 Quickview를 사용할 수 있습니다. Quickview는 두 번째 창이나 책 목록 안의 패널을 열어 관심 값과 일치하는 책 목록을 보여 줍니다. 예를 들어 현재 선택한 책의 저자 중 하나 이상을 공유하는 모든 책 목록을 보고 싶다고 가정해 봅시다. 관심 있는 저자 셀을 클릭한 뒤 ‘Q’ 키를 누르거나 calibre 창의 Layout 영역에 있는 Quickview 아이콘을 클릭하십시오. 왼쪽에는 그 책의 모든 저자가, 오른쪽에는 선택한 저자의 모든 책이 표시되는 창 또는 패널이 열립니다.

Quickview의 활용 예를 들면, 다음과 같은 다른 책을 빠르게 확인할 수 있습니다:

- 현재 선택한 책에 적용된 태그가 같은 책

- 현재 책과 같은 시리즈에 있는 책

- 현재 책과 같은 사용자 정의 열 값을 가진 책

- 현재 책과 같은 저자 중 한 명이 쓴 책

- 사용자 정의 열 값을 공유하는 책

Quickview 정보가 표시되는 위치는 두 가지입니다:

1. “도킹 해제” 상태로 열 수 있습니다. 이 경우 calibre 창 위에 떠 있으며 사용자가 명시적으로 닫을 때까지 열려 있습니다.

2. “도킹” 상태로 열 수 있습니다. 이 경우 calibre 메인 창의 책 목록 영역 안의 패널로 열립니다.

“Dock/Undock” 버튼을 사용하여 필요에 따라 도킹 상태와 도킹 해제 상태 사이를 전환할 수 있습니다.

Quickview 패널은 계속 열어 둘 수 있으며, 이 경우 책 목록의 움직임을 따라갑니다. 예를 들어 calibre 라이브러리 보기에서 어떤 책의 카테고리 열(태그, 시리즈, 출판사, 저자 등)을 클릭하면 Quickview 창 내용이 바뀌어 왼쪽 패널에는 선택한 책의 해당 카테고리 값들(예: 그 책의 태그)이 표시됩니다. 그 목록의 첫 번째 항목이 선택되고, 오른쪽 패널에는 라이브러리에서 그 값을 사용하는 모든 책이 표시됩니다. 왼쪽 패널에서 다른 값을 클릭하면 그 다른 값을 가진 책을 볼 수 있습니다.

Quickview 창에서 책을 두 번 클릭하면 라이브러리 보기에서 그 책이 선택됩니다. 그러면 QuickView 창(왼쪽 패널)의 항목 표시도 바뀌어 새로 선택한 책의 항목을 보여 줍니다.

Quickview 창에서 책을 Shift- 또는 Ctrl- 더블클릭하면 calibre 창에서 그 책의 메타데이터 편집 대화 상자가 열립니다. 대화 상자를 닫으면 편집한 책이 Quickview 대상으로 표시됩니다.

열 머리글 위에 마우스를 올려 툴팁을 보면 그 열이 Quickview 가능한지 확인할 수 있습니다. 또한 열 머리글을 마우스 오른쪽 버튼으로 클릭하여 메뉴에 “Quickview” 옵션이 표시되는지 확인할 수도 있습니다. 이 메뉴의 Quickview를 선택하는 것은 현재 셀에서 ‘Q’를 누르는 것과 같습니다.

옵션(Preferences → Look & feel → Quickview 안):

- 현재 가상 라이브러리를 존중할지 여부. 체크하면 Quickview는 현재 가상 라이브러리에 있는 책만 보여 줍니다. 기본값: 가상 라이브러리 존중

- 커서 키로 책 목록의 열이 바뀔 때 Quickview 창 내용도 바꿀지 여부. 기본값: 커서 키로 바뀐 내용은 따라가지 않음

- Quickview 창에서 셀을 더블클릭했을 때 “빠르게 보기” 중인 열도 바꿀지 여부. 그렇지 않으면 책은 바뀌지만 조사 중인 열은 바뀌지 않습니다. 기본값: 열 바꾸기

- Quickview 패널에서 Enter 키를 눌렀을 때 “빠르게 보기” 중인 열을 현재 열로 바꿀지 여부. 그렇지 않으면 책은 바뀌지만 조사 중인 열은 바뀌지 않습니다. 기본값: 열 바꾸기

- Quickview 창/패널에 어떤 열을 표시할지 선택합니다.

작업

작업 패널은 현재 실행 중인 작업 수를 표시합니다. 작업은 별도 프로세스에서 실행되는 태스크입니다. 전자책 변환이나 리더 장치와의 통신 등이 여기에 포함됩니다. 작업 패널을 클릭하면 작업 목록에 접근할 수 있습니다. 작업이 완료되면 목록에서 해당 작업을 두 번 클릭하여 상세 로그를 볼 수 있습니다. 정상적으로 완료되지 않았을 수 있는 작업을 디버그할 때 유용합니다.

키보드 단축키
calibre에는 시간과 마우스 이동을 줄여 주는 여러 키보드 단축키가 있습니다. 이 단축키는 책 목록 보기에서(특정 책의 상세 정보를 편집하고 있지 않을 때) 활성화되며, 대부분은 현재 선택한 제목에 영향을 줍니다. calibre 전자책 뷰어는 자체 단축키를 가지고 있으며, 뷰어의 :guilabel:`Preferences`에서 사용자 지정할 수 있습니다.

참고

참고: calibre의 키보드 단축키는 특별히 언급된 경우가 아니면 수정 키(Command, Option, Control 등)가 필요하지 않습니다. 예를 들어 편집하려면 E처럼 문자 키만 누르면 됩니다.

calibre 메인 프로그램의 키보드 단축키

키보드 단축키

동작

F2 (macOS의 경우 입력)

책 목록에서 현재 선택한 필드의 메타데이터를 편집합니다.

A

책 추가하기

Shift+A

선택한 책에 형식 추가

C

선택한 책 변환

D

장치로 보내기

Del

선택한 책 제거

E

선택한 책의 메타데이터 편집하기

G

책 가져오기

I

책 상세정보 보기

K

목차 편집하기

M

선택한 레코드 병합

Alt+M

선택한 레코드 병합, 원본 유지

O

담고 있는 폴더 열기

P

책 다듬기

S

디스크에 저장하기

T

책 편집

V

보기

Shift+V

마지막으로 읽은 책 보기

Ctrl+Alt+A

현재 선택한 책에 대해 AI에게 묻기

macOS의 경우 Alt+V/Cmd+V

형식 지정해서 보기

Alt+Shift+J

작업 목록 전환

Alt+Shift+B

표지 브라우저 전환

Alt+Shift+D

책 상세정보 패널 전환

Alt+Shift+T

태그 탐색기 전환

Alt+Shift+G

표지 그리드 전환

Alt+Shift+H

책장 보기 전환

Alt+A

현재 책과 같은 저자의 책 표시

Alt+T

현재 책과 같은 태그를 가진 책 표시

Alt+P

현재 책과 같은 출판사의 책 표시

Alt+Shift+S

현재 책과 같은 시리즈의 책 표시

/, Ctrl+F

검색 표시줄로 포커스 이동

Shift+Ctrl+F

고급 검색 대화 상자 열기

Shift+Alt+F

검색 표시줄 전환

Esc

지금 검색결과 비우기

Shift+Esc

책 목록으로 포커스 이동

Ctrl+Esc

가상 라이브러리 지우기

Alt+Esc

“그외 사용 가능한 검색 제한” 지우기

Ctrl+*

현재 검색을 기준으로 임시 가상 라이브러리 만들기

Ctrl+Tab

다음 가상 라이브러리 탭 선택

Ctrl+Shift+Tab

이전 가상 라이브러리 탭 선택

N 또는 F3

현재 검색과 일치하는 다음 책 찾기(검색 환경설정에서 검색 강조가 켜져 있을 때만 동작)

Shift+N 또는 Shift+F3

현재 검색과 일치하는 이전 책 찾기(검색 환경설정에서 검색 강조가 켜져 있을 때만 동작)

Ctrl+D

메타데이터와 표지 다운로드

Ctrl+R

캘리버 재시작

Ctrl+Shift+R

디버그 모드에서 캘리버 재시작

Shift+Ctrl+E

calibre에 빈 책 추가

Ctrl+M

선택한 책의 표시/표시 해제 상태 전환

Ctrl+/ 또는 Ctrl+Alt+F

라이브러리의 모든 책 전체 텍스트를 검색하는 팝업 열기

Q

관련 시리즈/태그 등을 보는 Quick View 팝업 열기

Shift+Q

열린 Quick View 패널로 포커스 이동

Shift+S

Quick View 패널에서 검색 수행

F5

현재 정렬 다시 적용

Ctrl+Q

나가기

X

책 목록 자동 스크롤 전환

Ctrl+Alt+Shift+F

:guilabel:`Tag browser`에 현재 표시된 카테고리에 속한 책만 표시하도록 제한

B

라이브러리의 모든 책에 대해 calibre 전자책 뷰어에서 만든 주석(하이라이트 및 책갈피) 둘러보기

Ctrl+Shift+N

저자/시리즈/태그 등과 연결된 메모 둘러보기

Alt+Shift+L

넓은 보기와 좁은 보기 사이에서 레이아웃 전환

Ctrl+Alt+P

이전 라이브러리로 전환(있는 경우)

Ctrl+Alt+Shift+P

이전 가상 라이브러리로 전환(있는 경우)

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 즐겨찾는 뉴스 웹사이트 추가

즐겨찾는 뉴스 웹사이트 추가
calibre는 인터넷에서 뉴스를 다운로드하고 전자책으로 변환하는 강력하고 유연하며 사용하기 쉬운 프레임워크를 제공합니다. 다음에서는 예시를 통해 다양한 웹사이트에서 뉴스를 가져오는 방법을 보여줍니다.

프레임워크 사용법을 이해하려면 아래에 나열된 순서대로 예시를 따르세요:

- 완전 자동 가져오기

- calibre 블로그

- bbc.co.uk

- 가져오기 프로세스 맞춤 설정

- bbc.co.uk 인쇄 버전 사용하기

- 기사 스타일 대체

- 자르고 다듬기

- 실제 예시

- 새 레시피 개발 팁

- 더 읽을 거리

- API 문서

완전 자동 가져오기

뉴스 출처가 충분히 단순하다면 calibre가 완전 자동으로 뉴스를 가져올 수 있습니다. URL을 제공하기만 하면 됩니다. calibre는 뉴스 출처를 다운로드하는 데 필요한 모든 정보를 :term:`레시피`에 수집합니다. calibre에 뉴스 출처를 알려주려면 해당 뉴스 출처에 대한 :term:`레시피`를 만들어야 합니다. 몇 가지 예시를 살펴보겠습니다:

calibre 블로그
calibre 블로그는 새로운 calibre 사용자에게 유용한 calibre 기능을 간단하고 접근하기 쉽게 설명하는 게시물 블로그입니다. 이 블로그를 전자책으로 다운로드하려면 블로그의 RSS 피드를 사용합니다:

http://blog.calibre-ebook.com/feeds/posts/default

블로그 페이지 하단의 “구독하기”에서 게시물 → Atom`을 선택하여 RSS URL을 얻었습니다. calibre가 피드를 다운로드하고 전자책으로 변환하도록 하려면 :guilabel:`뉴스 가져오기 버튼을 마우스 오른쪽 버튼 클릭한 다음 사용자 지정 뉴스 소스 추가 메뉴 항목을 클릭하고 새 레시피 버튼을 클릭하세요. 아래와 유사한 대화상자가 열립니다.

먼저 레시피 제목 필드에 ``Calibre Blog``를 입력하세요. 이것이 위 피드의 기사로 생성될 전자책의 제목이 됩니다.

다음 두 필드(가장 오래된 기사`와 :guilabel:`피드당 최대 기사 수)는 각 피드에서 다운로드할 기사 수를 제어할 수 있으며, 의미를 쉽게 유추할 수 있습니다.

레시피에 피드를 추가하려면 피드 제목과 피드 URL을 입력하고 피드 추가 버튼을 클릭하세요. 피드를 추가한 후 저장 버튼을 클릭하면 완료입니다! 대화상자를 닫으세요.

새 레시피`를 테스트하려면 :guilabel:`뉴스 가져오기 버튼을 클릭하고 사용자 지정 뉴스 소스 하위 메뉴에서 calibre 블로그`를 클릭하세요. 몇 분 후, 새로 다운로드된 블로그 게시물 전자책이 기본 라이브러리 보기(리더가 연결되어 있으면 라이브러리 대신 리더에 저장됨)에 나타납니다. 선택하고 :guilabel:`보기 버튼을 눌러 읽으세요!

이렇게 적은 노력으로 잘 작동한 이유는 블로그가 전체 콘텐츠 RSS 피드를 제공하기 때문입니다. 즉, 기사 콘텐츠가 피드 자체에 포함되어 있습니다. 이러한 방식으로 전체 콘텐츠 피드를 제공하는 대부분의 뉴스 소스에서는 전자책으로 변환하기 위해 추가 노력이 필요하지 않습니다. 이제 전체 콘텐츠 피드를 제공하지 않는 뉴스 소스를 살펴보겠습니다. 이러한 피드에서 전체 기사는 웹페이지이며 피드에는 웹페이지 링크와 기사의 짧은 요약만 포함됩니다.

bbc.co.uk

*The BBC*의 다음 두 피드를 시도해 보겠습니다:

1. 뉴스 첫 페이지: https://newsrss.bbc.co.uk/rss/newsonline_world_edition/front_page/rss.xml

2. 과학/자연: https://newsrss.bbc.co.uk/rss/newsonline_world_edition/science/nature/rss.xml

위의 :ref:`calibre_blog`에서 설명한 절차를 따라 *The BBC*에 대한 레시피를 만드세요(위의 피드 사용). 다운로드한 전자책을 살펴보면 calibre가 각 기사 웹페이지에서 관심 있는 콘텐츠만 추출하는 훌륭한 작업을 수행한 것을 알 수 있습니다. 그러나 추출 과정이 완벽하지는 않습니다. 때때로 메뉴나 탐색 도구와 같은 원치 않는 콘텐츠가 남거나 기사 머리글과 같이 남겨져야 할 콘텐츠가 제거됩니다. 완벽한 콘텐츠 추출을 위해서는 다음 섹션에서 설명하는 것처럼 가져오기 과정을 맞춤 설정해야 합니다.

가져오기 프로세스 맞춤 설정

다운로드 과정을 완벽하게 하거나 특히 복잡한 웹사이트에서 콘텐츠를 다운로드하려면 레시피 프레임워크의 모든 기능과 유연성을 활용할 수 있습니다. 그렇게 하려면 사용자 지정 뉴스 소스 추가 대화상자에서 고급 모드로 전환 버튼을 클릭하세요.

가장 쉽고 종종 가장 효과적인 맞춤 설정은 온라인 기사의 인쇄 버전을 사용하는 것입니다. 인쇄 버전은 일반적으로 불필요한 요소가 훨씬 적고 전자책으로 변환이 훨씬 원활합니다. The BBC 기사의 인쇄 버전을 사용해 보겠습니다.

bbc.co.uk 인쇄 버전 사용하기

첫 번째 단계는 이전에 :ref:`bbc`에서 다운로드한 전자책을 살펴보는 것입니다. 전자책의 각 기사 끝에 기사가 어디서 다운로드되었는지 알려주는 작은 설명이 있습니다. 해당 URL을 복사하여 브라우저에 붙여넣으세요. 이제 기사 웹페이지에서 “인쇄 가능한 버전”을 가리키는 링크를 찾으세요. 클릭하여 기사의 인쇄 버전을 확인하세요. 훨씬 깔끔합니다! 이제 두 URL을 비교해 보세요. 제 경우는 다음과 같습니다:

기사 URL

https://news.bbc.co.uk/2/hi/science/nature/7312016.stm

인쇄 버전 URL

https://newsvote.bbc.co.uk/mpapps/pagetools/print/news.bbc.co.uk/2/hi/science/nature/7312016.stm

인쇄 버전을 얻으려면 모든 기사 URL 앞에 다음을 붙여야 합니다:

newsvote.bbc.co.uk/mpapps/pagetools/print/

이제 사용자 지정 뉴스 소스 대화상자의 :guilabel:`고급 모드`에서 다음과 같은 내용이 표시됩니다(고급 모드로 전환하기 전에 The BBC 레시피를 선택하세요):

:guilabel:`기본 모드`의 필드가 간단한 방식으로 Python 코드로 변환된 것을 확인할 수 있습니다. 기사의 인쇄 버전을 사용하도록 이 레시피에 지시를 추가해야 합니다. 다음 두 줄만 추가하면 됩니다:

def print_version(self, url):
return url.replace('https://', 'https://newsvote.bbc.co.uk/mpapps/pagetools/print/')

Python이므로 들여쓰기가 중요합니다. 줄을 추가한 후 다음과 같아야 합니다:

위에서 ``def print_version(self, url)``은 calibre가 각 기사에 대해 호출하는 *메서드*를 정의합니다. ``url``은 원본 기사의 URL입니다. ``print_version``은 해당 URL을 가져와 기사의 인쇄 버전을 가리키는 새 URL로 대체합니다. `Python <https://www.python.org>`_에 대해 알아보려면 `튜토리얼 <https://docs.python.org/tutorial/>`_을 참조하세요.

이제 레시피 추가/업데이트 버튼을 클릭하면 변경 사항이 저장됩니다. 전자책을 다시 다운로드하세요. 훨씬 개선된 전자책을 얻을 수 있어야 합니다. 새 버전의 문제 중 하나는 인쇄 버전 웹페이지의 글꼴이 너무 작다는 것입니다. 전자책으로 변환할 때 자동으로 수정되지만 수정 과정 후에도 메뉴와 탐색 모음의 글꼴 크기가 기사 텍스트에 비해 너무 커집니다. 이 문제를 해결하기 위해 다음 섹션에서 추가 맞춤 설정을 하겠습니다.

기사 스타일 대체

이전 섹션에서 The BBC 인쇄 버전 기사의 글꼴 크기가 너무 작은 것을 보았습니다. *The BBC*를 포함한 대부분의 웹사이트에서 이 글꼴 크기는 CSS 스타일시트로 설정됩니다. 다음 줄을 추가하여 이러한 스타일시트의 가져오기를 비활성화할 수 있습니다:

no_stylesheets = True

이제 레시피는 다음과 같습니다:

새 버전은 상당히 좋아 보입니다. 완벽주의자라면 다운로드한 콘텐츠를 실제로 수정하는 방법을 다루는 다음 섹션을 읽고 싶을 것입니다.

자르고 다듬기
calibre에는 다운로드한 콘텐츠를 조작하는 매우 강력하고 유연한 기능이 포함되어 있습니다. 몇 가지를 보여주기 위해 오랜 친구인 The BBC 레시피를 다시 살펴보겠습니다. 몇몇 기사(인쇄 버전)의 소스 코드(HTML)를 살펴보면 유용한 정보가 전혀 없는 꼬리말이 포함되어 있습니다

<div class="footer">
...
</div>

다음을 추가하여 제거할 수 있습니다:

remove_tags = [dict(name='div', attrs={'class':'footer'})]

마지막으로, 이전에 비활성화한 CSS 일부를 전자책 변환에 적합한 자체 :term:`CSS`로 대체합니다:

extra_css = '.headline {font-size: x-large;} \n .fact { padding-top: 10pt }'

이러한 추가로 인해 레시피가 “프로덕션 품질”이 되었습니다.

이 :term:`레시피`는 calibre의 능력 중 빙산의 일각만을 보여줍니다. calibre의 더 많은 능력을 탐색하기 위해 다음 섹션에서 더 복잡한 실제 예시를 살펴보겠습니다.

실제 예시

``BasicNewsRecipe``의 :term:`API`를 더 많이 활용하는 꽤 복잡한 실제 예시는 *The New York Times*에 대한 :term:`레시피`입니다
import string, re from calibre import strftime from calibre.web.feeds.recipes import BasicNewsRecipe from calibre.ebooks.BeautifulSoup import BeautifulSoup class NYTimes(BasicNewsRecipe):

title = 'The New York Times'
__author__ = 'Kovid Goyal'
description = 'Daily news from the New York Times'
timefmt = ' [%a, %d %b, %Y]'
needs_subscription = True remove_tags_before = dict(id='article')
remove_tags_after = dict(id='article')
remove_tags = [dict(attrs={'class':['articleTools', 'post-tools', 'side_tool', 'nextArticleLink clearfix']}), dict(id=['footer', 'toolsRight', 'articleInline', 'navigation', 'archive', 'side_search', 'blog_sidebar', 'side_tool', 'side_index']), dict(name=['script', 'noscript', 'style'])]
encoding = 'cp1252'
no_stylesheets = True extra_css = 'h1 {font: sans-serif large;}\n.byline {font:monospace;}'

def get_browser(self):
br = BasicNewsRecipe.get_browser(self)
if self.username is not None and self.password is not None:
br.open('https://www.nytimes.com/auth/login')
br.select_form(name='login')
br['USERID'] = self.username br['PASSWORD'] = self.password br.submit()
return br def parse_index(self):
soup = self.index_to_soup('https://www.nytimes.com/pages/todayspaper/index.html')

def feed_title(div):
return ''.join(div.findAll(text=True, recursive=False)).strip()

articles = {}
key = None ans = []
for div in soup.findAll(True, attrs={'class':['section-headline', 'story', 'story headline']}):

if ''.join(div['class']) == 'section-headline':
key = string.capwords(feed_title(div))
articles[key] = []
ans.append(key)

elif ''.join(div['class']) in ['story', 'story headline']:
a = div.find('a', href=True)
if not a:
continue url = re.sub(r'\?.*', '', a['href'])
url += '?pagewanted=all'
title = self.tag_to_string(a, use_alt=True).strip()
description = ''
pubdate = strftime('%a, %d %b')
summary = div.find(True, attrs={'class':'summary'})
if summary:
description = self.tag_to_string(summary, use_alt=False)

feed = key if key is not None else 'Uncategorized'
if feed not in articles:
articles[feed] = []
if not 'podcasts' in url:
articles[feed].append( dict(title=title, url=url, date=pubdate, description=description, content=''))
ans = self.sort_index_by(ans, {'The Front Page':-1, 'Dining In, Dining Out':1, 'Obituaries':2})
ans = [(key, articles[key]) for key in ans if key in articles]
return ans def preprocess_html(self, soup):
refresh = soup.find('meta', {'http-equiv':'refresh'})
if refresh is None:
return soup content = refresh.get('content').partition('=')[2]
raw = self.browser.open('https://www.nytimes.com'+content).read()
return BeautifulSoup(raw.decode('cp1252', 'replace'))

이 :term:`레시피`에서 여러 새로운 기능을 볼 수 있습니다. 먼저:

timefmt = ' [%a, %d %b, %Y]'

생성된 전자책의 첫 페이지에 표시되는 시간을 요일, 일 번호 월, 연도 형식으로 설정합니다. :attr:`timefmt <calibre.web.feeds.news.BasicNewsRecipe.timefmt>`를 참조하세요.

그런 다음 다운로드한 :term:`HTML`을 정리하기 위한 지시문 그룹이 표시됩니다:

remove_tags_before = dict(name='h1')
remove_tags_after = dict(id='footer')
remove_tags = ...

첫 번째 <h1> 태그 이전의 모든 것과 id가 ``footer``인 첫 번째 태그 이후의 모든 것을 제거합니다. remove_tags, remove_tags_before, :attr:`remove_tags_after <calibre.web.feeds.news.BasicNewsRecipe.remove_tags_after>`를 참조하세요.

다음으로 흥미로운 기능은:

needs_subscription = True
...
def get_browser(self):
...

``needs_subscription = True``는 이 레시피가 콘텐츠에 접근하려면 사용자 이름과 암호가 필요하다는 것을 calibre에 알려줍니다. 이로 인해 calibre는 이 레시피를 사용할 때마다 사용자 이름과 암호를 요청합니다. :meth:`calibre.web.feeds.news.BasicNewsRecipe.get_browser`의 코드가 실제로 NYT 웹사이트에 로그인합니다. 로그인하면 calibre는 동일한 로그인된 브라우저 인스턴스를 사용하여 모든 콘텐츠를 가져옵니다. ``get_browser``의 코드를 이해하려면 `mechanize <https://mechanize.readthedocs.io/en/latest/>`_를 참조하세요.

다음 새로운 기능은 calibre.web.feeds.news.BasicNewsRecipe.parse_index() 메서드입니다. 이 메서드는 https://www.nytimes.com/pages/todayspaper/index.html로 이동하여 오늘 신문에 실린 기사 목록을 가져오는 역할을 합니다. RSS`를 단순히 사용하는 것보다 복잡하지만, 이 레시피는 그날의 신문에 매우 밀접하게 대응하는 전자책을 생성합니다. ``parse_index``는 일간지 웹페이지를 파싱하기 위해 `BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/>`_을 많이 사용합니다. BeautifulSoup이 마음에 들지 않으면 다른 더 현대적인 파서를 사용할 수도 있습니다. calibre에는 권장 파서인 `lxml <https://lxml.de/>`_과 `html5lib <https://github.com/html5lib/html5lib-python>`_이 포함되어 있습니다. 이를 사용하려면 ``index_to_soup()` 호출을 다음으로 바꾸세요:

raw = self.index_to_soup(url, raw=True)
# For html5lib
import html5lib
root = html5lib.parse(raw, namespaceHTMLElements=False, treebuilder='lxml')
# For the lxml html 4 parser
from lxml import html
root = html.fromstring(raw)

마지막 새로운 기능은 calibre.web.feeds.news.BasicNewsRecipe.preprocess_html() 메서드입니다. 다운로드한 모든 HTML 페이지에서 임의의 변환을 수행하는 데 사용할 수 있습니다. 여기서는 NYTimes가 각 기사 앞에 표시하는 광고를 우회하는 데 사용됩니다.

새 레시피 개발 팁

새 레시피를 개발하는 가장 좋은 방법은 명령줄 인터페이스를 사용하는 것입니다. 좋아하는 Python 편집기를 사용하여 레시피를 만들고 myrecipe.recipe 파일에 저장하세요. .recipe 확장자가 필요합니다. 다음 명령으로 이 레시피를 사용하여 콘텐츠를 다운로드할 수 있습니다:

ebook-convert myrecipe.recipe .epub --test -vv --debug-pipeline debug

ebook-convert 명령은 모든 웹페이지를 다운로드하여 EPUB 파일 myrecipe.epub`에 저장합니다. `-vv`` 옵션은 ebook-convert가 수행 중인 작업에 대한 많은 정보를 출력합니다. ebook-convert-recipe-input --test 옵션은 최대 두 개의 피드에서 몇 개의 기사만 다운로드합니다. 또한 ebook-convert는 다운로드한 HTML을 ebook-convert --debug-pipeline 옵션에서 지정한 debug/input 폴더에 저장합니다.

다운로드가 완료되면 파일 :file:`debug/input/index.html`을 브라우저에서 열어 다운로드한 :term:`HTML`을 확인할 수 있습니다. 다운로드와 전처리가 올바르게 수행되고 있다고 확인되면 아래와 같이 다양한 형식의 전자책을 생성할 수 있습니다:

ebook-convert myrecipe.recipe myrecipe.epub
ebook-convert myrecipe.recipe myrecipe.mobi
...

레시피에 만족하고 내장 레시피 세트에 포함될 만큼 충분한 수요가 있다고 생각되면 `calibre 레시피 포럼 <https://www.mobileread.com/forums/forumdisplay.php?f=228>`_에 레시피를 게시하여 다른 calibre 사용자와 공유하세요.

참고
macOS에서는 명령줄 도구가 calibre 번들 안에 있습니다. 예를 들어, calibre를 /Applications`에 설치한 경우 명령줄 도구는 :file:/Applications/calibre.app/Contents/MacOS/`에 있습니다.

더 보기

ebook-convert

모든 전자책 변환을 위한 명령줄 인터페이스입니다.

더 읽을 거리

``BasicNewsRecipe``에서 사용할 수 있는 일부 기능을 사용하여 고급 레시피를 작성하는 방법에 대해 자세히 알아보려면 다음 출처를 참조하세요:

API 문서

BasicNewsRecipe 클래스와 모든 중요한 메서드 및 필드에 대한 문서입니다.

기본 뉴스 레시피 [https://github.com/kovidgoyal/calibre/blob/master/src/calibre/web/feeds/news.py]

``BasicNewsRecipe``의 소스 코드

기본 제공 레시피 [https://github.com/kovidgoyal/calibre/tree/master/recipes]

calibre에 포함된 내장 레시피의 소스 코드

캘리버 레시피 포럼 [https://www.mobileread.com/forums/forumdisplay.php?f=228]

지식이 풍부한 많은 calibre 레시피 작성자들이 이곳에서 활동합니다.

API 문서
레시피 API 문서BasicNewsRecipe

BasicNewsRecipe.adeify_images()
BasicNewsRecipe.image_url_processor()
BasicNewsRecipe.print_version()
BasicNewsRecipe.tag_to_string()
BasicNewsRecipe.abort_article()
BasicNewsRecipe.abort_recipe_processing()
BasicNewsRecipe.add_toc_thumbnail()
BasicNewsRecipe.canonicalize_internal_url()
BasicNewsRecipe.cleanup()
BasicNewsRecipe.clone_browser()
BasicNewsRecipe.default_cover()
BasicNewsRecipe.download()
BasicNewsRecipe.extract_readable_article()
BasicNewsRecipe.get_article_url()
BasicNewsRecipe.get_browser()
BasicNewsRecipe.get_cover_url()
BasicNewsRecipe.get_extra_css()
BasicNewsRecipe.get_feeds()
BasicNewsRecipe.get_masthead_title()
BasicNewsRecipe.get_masthead_url()
BasicNewsRecipe.get_obfuscated_article()
BasicNewsRecipe.get_url_specific_delay()
BasicNewsRecipe.index_to_soup()
BasicNewsRecipe.is_link_wanted()
BasicNewsRecipe.parse_feeds()
BasicNewsRecipe.parse_index()
BasicNewsRecipe.populate_article_metadata()
BasicNewsRecipe.postprocess_book()
BasicNewsRecipe.postprocess_html()
BasicNewsRecipe.preprocess_html()
BasicNewsRecipe.preprocess_image()
BasicNewsRecipe.preprocess_raw_html()
BasicNewsRecipe.publication_date()
BasicNewsRecipe.skip_ad_pages()
BasicNewsRecipe.sort_index_by()
BasicNewsRecipe.articles_are_obfuscated
BasicNewsRecipe.auto_cleanup
BasicNewsRecipe.auto_cleanup_keep
BasicNewsRecipe.browser_type
BasicNewsRecipe.center_navbar
BasicNewsRecipe.compress_news_images

BasicNewsRecipe.compress_news_images_auto_size

BasicNewsRecipe.compress_news_images_max_size
BasicNewsRecipe.conversion_options
BasicNewsRecipe.cover_margins
BasicNewsRecipe.delay
BasicNewsRecipe.description
BasicNewsRecipe.encoding
BasicNewsRecipe.extra_css
BasicNewsRecipe.feeds
BasicNewsRecipe.filter_regexps
BasicNewsRecipe.handle_gzip
BasicNewsRecipe.ignore_duplicate_articles
BasicNewsRecipe.keep_only_tags
BasicNewsRecipe.language
BasicNewsRecipe.masthead_url
BasicNewsRecipe.match_regexps
BasicNewsRecipe.max_articles_per_feed
BasicNewsRecipe.needs_subscription
BasicNewsRecipe.no_stylesheets
BasicNewsRecipe.oldest_article
BasicNewsRecipe.preprocess_regexps
BasicNewsRecipe.publication_type
BasicNewsRecipe.recipe_disabled
BasicNewsRecipe.recipe_specific_options
BasicNewsRecipe.recursions
BasicNewsRecipe.remove_attributes
BasicNewsRecipe.remove_empty_feeds
BasicNewsRecipe.remove_javascript
BasicNewsRecipe.remove_tags
BasicNewsRecipe.remove_tags_after
BasicNewsRecipe.remove_tags_before
BasicNewsRecipe.requires_version
BasicNewsRecipe.resolve_internal_links
BasicNewsRecipe.reverse_article_order
BasicNewsRecipe.scale_news_images
BasicNewsRecipe.scale_news_images_to_device
BasicNewsRecipe.simultaneous_downloads
BasicNewsRecipe.summary_length
BasicNewsRecipe.template_css
BasicNewsRecipe.timefmt
BasicNewsRecipe.timeout
BasicNewsRecipe.title
BasicNewsRecipe.use_embedded_content

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 즐겨찾는 뉴스 웹사이트 추가 »
- 레시피 API 문서

레시피 API 문서

레시피 작성을 위한 API는 :class:`BasicNewsRecipe`에 의해 정의됩니다.

class calibre.web.feeds.news.BasicNewsRecipe(options, log, progress_reporter)[소스]

모든 레시피에 필요한 논리를 포함하는 기본 클래스입니다. 이 클래스의 기능들을 하나하나 수정하여 덮어써나감으로써 점점 더 사용자에게 알맞으면서도 강력한 레시피로 만들어 갈 수 있습니다. 레시피 작성에 대한 튜토리얼 소개는 :doc:`news`를 참조하세요.

classmethod adeify_images(soup)[소스]

레시피가 생성한 EPUB를 Adobe Digital Editions로 보았을 때 이미지 표시에 문제가 있다면 postprocess_html() 메서드 안에서 이 메서드를 호출하세요.

classmethod image_url_processor(baseurl, url)[소스]

이미지 URL에 대해 일부 처리를 수행합니다(예: 동적으로 생성된 이미지의 크기 제한 제거 등). 처리된 URL을 반환합니다. 이미지 가져오기를 건너뛰려면 None이나 빈 문자열을 반환하세요.

classmethod print_version(url)[소스]

기사 콘텐츠가 있는 웹페이지를 가리키는 `url`을 받아, 기사의 인쇄 버전을 가리키는 :term:`URL`을 반환합니다. 기본적으로 아무 작업도 수행하지 않습니다. 예:

def print_version(self, url):
return url + '?&pagewanted=print'

classmethod tag_to_string(tag, use_alt=True, normalize_whitespace=True)[소스]

BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/] :code:`Tag`를 받아 CDATA 섹션과 alt 태그 속성을 포함하여 텍스트를 재귀적으로 추출하는 편의 메서드입니다. 비어 있을 수도 있는 유니코드 문자열을 반환합니다.

use_alt: `True`이면 텍스트 콘텐츠가 없는 태그에 대해 alt 속성을 사용하려고 시도합니다
tag: BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/]
Tag

abort_article(msg=None)[소스]

전처리 메서드 중 하나 안에서 이 메서드를 호출하여 현재 기사의 다운로드를 중단하세요. 순수 동영상 기사와 같이 부적절한 콘텐츠가 포함된 기사를 건너뛰는 데 유용합니다.

abort_recipe_processing(msg)[소스]

레시피 다운로드 시스템이 이 레시피의 다운로드를 중단하도록 하며, 사용자에게 간단한 피드백 메시지를 표시합니다.

add_toc_thumbnail(article, src)[소스]

기사 안에 있는 <img> 태그 중 목차에서 기사를 나타내는 썸네일 이미지로서 사용하기 적합한 것을 src 속성으로 하여 이 메서드를 populate_article_metadata 내부에서 호출하세요. 썸네일 이미지가 실제로 사용될지 여부는 장치마다 다릅니다(현재 Kindle에서만 사용). 참조되는 이미지는 성공적으로 다운로드된 것이어야 합니다. 그렇지 않으면 무시됩니다.

canonicalize_internal_url(url, is_link=True)[소스]

``url``의 정규 표현 집합을 반환합니다. 기본 구현은 쿼리 매개 변수, 프래그먼트 등을 무시하고 URL의 서버 호스트명과 경로만 사용합니다. 정규 표현은 이 뉴스 소스의 모든 URL에서 고유해야 합니다. 고유하지 않으면 내부 링크가 잘못 해석될 수 있습니다.

매개변수:

is_link – URL이 HTML 파일의 내부 링크에서 오는 경우 True입니다. URL이 기사를 다운로드하는 데 사용된 URL인 경우 False입니다.

cleanup()[소스]

모든 기사가 다운로드된 후에 호출됩니다. 구독 사이트에서 로그아웃하기 등과 같은 정리 작업을 수행하는 데 사용하세요.

clone_browser(br)[소스]

브라우저 br을 복제합니다. 복제된 브라우저는 멀티스레드 다운로드에 사용됩니다(mechanize가 thread-safe하지 않기 때문). 기본 복제 루틴이 대부분의 사용자 맞춤 설정을 잡아내야 합니다만, 레시피 안에서 특이한 방식을 사용하고 있다면 이 메서드를 레시피 안에서 덮어쓰고 수동으로 복제하세요.

복제된 브라우저 인스턴스는 쿠키 처리를 별도로 맞춤 설정하지 않았다면 기본값으로 동일한 thread-safe CookieJar를 사용합니다.

default_cover(cover_file)[소스]

표지가 없는 레시피에 대한 일반 표지 만들기

download()[소스]

이 레시피의 피드에 해당하는 모든 기사를 다운로드하고 전처리합니다. 이 메서드는 특정 레시피 인스턴스에서 단 한 번만 호출되어야 합니다. 한 번 이상 호출하면 정의되지 않은 동작이 발생할 수 있습니다. :return: index.html 경로

extract_readable_article(html, url)[소스]

‘html’에서 주요 기사 콘텐츠를 추출하고 정리한 후 (article_html, extracted_title) 튜플로 반환합니다. Arc90의 원래 가독성 알고리즘을 기반으로 합니다.

get_article_url(article)[소스]

서브클래스에서 덮어써서 각 기사의 콘텐츠를 가리키는 :term:`URL`의 추출을 맞춤 설정하세요. 기사 URL을 반환합니다. 피드에서 파싱된 기사를 나타내는 객체인 `article`과 함께 호출됩니다. `feedparser <https://pythonhosted.org/feedparser/>`_를 참조하세요. 기본적으로 원본 링크를 찾습니다(FeedBurner나 Pheedo와 같은 서비스를 통해 신디케이트된 피드의 경우). 찾으면 해당 링크를 반환하고, 그렇지 않으면 `article.link <https://pythonhosted.org/feedparser/reference-entry-link.html>`_를 반환합니다.

get_browser(*args, **kwargs)[소스]

웹에서 문서를 가져오는 데 사용되는 브라우저 인스턴스를 반환합니다. 기본적으로 쿠키를 지원하고, robots.txt를 무시하고, 새로고침을 처리하며, 임의의 일반 사용자 에이전트를 가진 mechanize [https://mechanize.readthedocs.io/en/latest/] 브라우저 인스턴스를 반환합니다.

브라우저를 맞춤 설정하려면 서브클래스에서 이 메서드를 다음과 같이 덮어쓰세요:

def get_browser(self, *a, **kw):
br = super().get_browser(*a, **kw)
# Add some headers
br.addheaders += [
('My-Header', 'one'),
('My-Header2', 'two'),
]
# Set some cookies
br.set_cookie('name', 'value')
br.set_cookie('name2', 'value2', domain='.mydomain.com')
# Make a POST request with some data
br.open('https://someurl.com', {'username': 'def', 'password': 'pwd'}).read()
# Do a login via a simple web form (only supported with mechanize browsers)
if self.username is not None and self.password is not None:
br.open('https://www.nytimes.com/auth/login')
br.select_form(name='login')
br['USERID'] = self.username
br['PASSWORD'] = self.password
br.submit()
return br

get_cover_url()[소스]

이 호의 표지 이미지에 대한 URL`을 반환하거나 `None`을 반환합니다. 기본적으로 보통 `None`인 `self.cover_url 멤버의 값을 반환합니다. 레시피가 전자책의 표지를 다운로드하도록 하려면 서브클래스에서 이 메서드를 덮어쓰거나, 이 메서드가 호출되기 전에 멤버 변수 `self.cover_url`을 설정하세요.

get_extra_css()[소스]

기본적으로 `self.extra_css`를 반환합니다. 프로그래밍 방식으로 extra_css를 생성하려면 덮어쓰세요.

get_feeds()[소스]

이 프로필에 대해 가져올 RSS 피드 목록을 반환합니다. 목록의 각 요소는 (제목, url) 형식의 2요소 튜플이어야 합니다. 제목이 None이거나 빈 문자열이면 피드의 제목이 사용됩니다. 이 메서드는 레시피가 다운로드할 피드 목록을 파악하기 위해 일부 처리를 수행해야 하는 경우에 유용합니다. 그렇다면 서브클래스에서 덮어쓰세요.

get_masthead_title()[소스]

서브클래스에서 덮어써서 레시피 제목 대신 다른 것을 사용하세요

get_masthead_url()[소스]

이 호의 마스트헤드 이미지에 대한 URL`을 반환하거나 `None`을 반환합니다. 기본적으로 보통 `None`인 `self.masthead_url 멤버의 값을 반환합니다. 레시피가 전자책의 마스트헤드를 다운로드하도록 하려면 서브클래스에서 이 메서드를 덮어쓰거나, 이 메서드가 호출되기 전에 멤버 변수 `self.masthead_url`을 설정하세요. 마스트헤드 이미지는 Kindle MOBI 파일에서 사용됩니다.

get_obfuscated_article(url)[소스]

articles_are_obfuscated`를 설정하면 이 메서드가 모든 기사 URL에 대해 호출됩니다. 기사 HTML이 포함된 파일 시스템의 파일 경로를 반환해야 합니다. 해당 파일은 재귀적 HTML 가져오기 엔진에 의해 처리되므로 웹의 페이지/이미지에 대한 링크를 포함할 수 있습니다. 또는 {‘data’: <HTML 데이터>, ‘url’: <기사의 해석된 URL>} 형식의 딕셔너리를 반환할 수도 있습니다. 이렇게 하면 임시 파일을 생성할 필요가 없습니다. 예를 들어 리디렉션으로 인해 기사의 실제 URL이 이 메서드에 전달된 URL과 다른 경우 딕셔너리의 `url 키가 유용합니다. URL이 변경되지 않은 경우 생략할 수 있습니다.

이 메서드는 기사 콘텐츠에 자동으로 접근하기 어렵게 만드는 사이트에서 일반적으로 유용합니다.

get_url_specific_delay(url)[소스]

이 URL을 다운로드하기 전의 지연 시간(초)을 반환합니다. 지정된 URL에 대한 지연 시간을 프로그래밍 방식으로 결정하려면 서브클래스에서 이 메서드를 덮어쓰고, 영향을 주지 않으려는 URL에 대해서는 기본적으로 self.delay를 반환하세요.

반환:

부동소수점 숫자, 지연 시간(초).

index_to_soup(url_or_raw, raw=False, as_tree=False, save_raw=None)[소스]

인덱스 페이지의 URL을 받아 그에 대한 `BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/bs4/doc>`__을 반환하는 편의 메서드입니다.

url_or_raw: URL이거나 문자열로 된 다운로드된 인덱스 페이지

is_link_wanted(url, tag)[소스]

링크를 따라가야 하면 True를, 그렇지 않으면 False를 반환합니다. 기본적으로 NotImplementedError를 발생시켜 다운로더가 무시하도록 합니다.

매개변수:

- url – 따라갈 URL

- tag – URL이 파생된 태그

parse_feeds()[소스]

BasicNewsRecipe.get_feeds가 반환한 피드 목록에서 기사 목록을 생성합니다. Feed 객체 목록을 반환합니다.

parse_index()[소스]

이 메서드는 피드 대신 웹사이트를 파싱하여 기사 목록을 생성하는 레시피에서 구현해야 합니다. 일반적으로 현재 인쇄판의 모든 기사를 나열하는 “인쇄판” 웹페이지가 있는 뉴스 소스에 사용됩니다. 이 함수가 구현되면 :meth:`BasicNewsRecipe.parse_feeds`보다 우선하여 사용됩니다.

목록을 반환해야 합니다. 목록의 각 요소는 ('피드 제목', 기사 목록) 형식의 2요소 튜플이어야 합니다.

각 기사 목록은 다음 형식의 딕셔너리를 포함해야 합니다:

{
'title' : article title,
'url' : URL of print version,
'date' : The publication date of the article as a string,
'description' : A summary of the article
'content' : The full article (can be an empty string). Obsolete
do not use, instead save the content to a temporary
file and pass a file:///path/to/temp/file.html as
the URL.
}

예시는 `The Atlantic`을 다운로드하는 레시피를 참조하세요. 또한 기사의 저자에 대해 ‘author’를 추가할 수 있습니다.

어떤 이유로 처리를 중단하고 calibre가 오류 대신 사용자에게 간단한 메시지를 표시하도록 하려면 :meth:`abort_recipe_processing`을 호출하세요.

populate_article_metadata(article, soup, first)[소스]

기사에 속하는 각 HTML 페이지가 다운로드될 때 호출됩니다. 파싱된 HTML(soup)에서 저자/요약 등의 기사 메타데이터를 가져오는 데 사용하기 위한 것입니다.

매개변수:

- article – calibre.web.feeds.Article 클래스의 객체입니다. 요약을 변경했다면 text_summary도 함께 변경해야 합니다

- soup – 이 기사에 속하는 파싱된 HTML

- first – 파싱된 HTML이 기사의 첫 페이지인 경우에만 True입니다.

postprocess_book(oeb, opts, log)[소스]

파싱된 다운로드된 전자책에 대해 필요한 사후 처리를 수행합니다.

매개변수:

- oeb – OEBBook 객체

- opts – 변환 옵션

postprocess_html(soup, first_fetch)[소스]

이 메서드는 다운로드된 각 HTML 파일의 소스가 링크와 이미지를 위해 파싱된 후에 호출됩니다. :term:`HTML`에 대해 임의로 강력한 사후 처리를 수행하는 데 사용할 수 있습니다. 처리 후 `soup`를 반환해야 합니다.

매개변수:

- soup – 다운로드된 :term:`HTML`을 포함하는 `BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/>`__ 인스턴스.

- first_fetch – 기사의 첫 페이지이면 True입니다.

preprocess_html(soup)[소스]

이 메서드는 다운로드된 각 HTML 파일의 소스가 링크와 이미지를 위해 파싱되기 전에 호출됩니다. remove_tags 등으로 지정된 정리 후에 호출됩니다. :term:`HTML`에 대해 임의로 강력한 전처리를 수행하는 데 사용할 수 있습니다. 처리 후 `soup`를 반환해야 합니다.

soup: 다운로드된 :term:`HTML`을 포함하는 `BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/>`__ 인스턴스.

preprocess_image(img_data, image_url)[소스]

다운로드된 이미지 데이터에 대해 일부 처리를 수행합니다. 크기 조정이 수행되기 전에 원시 데이터에 대해 호출됩니다. 처리된 원시 데이터를 반환해야 합니다. 이미지를 건너뛰려면 None을 반환하세요.

preprocess_raw_html(raw_html, url)[소스]

이 메서드는 다운로드된 각 HTML 파일의 소스가 객체 트리로 파싱되기 전에 호출됩니다. raw_html은 웹에서 다운로드된 원시 HTML을 나타내는 유니코드 문자열입니다. url은 HTML이 다운로드된 URL입니다.

이 메서드는 preprocess_regexps 전에 작동합니다.

이 메서드는 처리된 raw_html을 유니코드 객체로 반환해야 합니다.

publication_date()[소스]

이 호가 발행된 날짜를 설정하려면 이 메서드를 사용합니다. 기본값은 다운로드 시점입니다. datetime.datetime 객체를 반환해야 합니다.

skip_ad_pages(soup)[소스]

이 메서드는 다운로드된 각 HTML 파일의 소스에 remove_tags, keep_only_tags와 같은 정리 속성이 적용되기 전에 호출됩니다. preprocess_regexps는 이미 적용되었을 것입니다. 레시피가 광고 페이지를 건너뛸 수 있도록 하기 위한 것입니다. soup가 광고 페이지를 나타내면 실제 페이지의 HTML을 반환하세요. 그렇지 않으면 None을 반환하세요.

soup: 다운로드된 :term:`HTML`을 포함하는 `BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/>`__ 인스턴스.

sort_index_by(index, weights)[소스]

`weights`에 따라 `index`의 제목을 정렬하는 편의 메서드입니다. `index`가 제자리에서 정렬됩니다. `index`를 반환합니다.

index: 제목 목록.

weights: 가중치를 제목에 매핑하는 딕셔너리입니다. index의 제목 중 weights에 없는 것이 있으면 가중치가 0으로 간주됩니다.

articles_are_obfuscated = False

True로 설정하고 :meth:`get_obfuscated_article`을 구현하여 콘텐츠 스크래핑을 어렵게 만드는 웹사이트를 처리하세요.

auto_cleanup = False

다운로드된 기사 페이지에서 모든 텍스트를 자동으로 추출합니다. readability 프로젝트의 알고리즘을 사용합니다. True로 설정하면 다운로드된 HTML을 수동으로 정리할 필요가 없습니다(수동 정리가 항상 더 우수하지만).

auto_cleanup_keep = None

자동 정리 알고리즘이 절대 제거하지 않아야 할 요소를 지정하세요. 구문은 XPath 표현식입니다. 예:

auto_cleanup_keep = '//div[@id="article-image"]' will keep all divs with
id="article-image"
auto_cleanup_keep = '//*[@class="important"]' will keep all elements
with class="important"
auto_cleanup_keep = '//div[@id="article-image"]|//span[@class="important"]'
will keep all divs with id="article-image" and spans
with class="important"

browser_type = 'mechanize'

서버에서 다운로드할 때 사용할 시뮬레이션된 브라우저 엔진입니다. 기본값은 로그인을 지원하는 Python mechanize 브라우저 엔진입니다. 그러나 로그인이 필요하지 않다면, 실제 Chromium 브라우저를 사용하여 네트워크 요청을 수행하는 ‘webengine’이나 Qt Networking 백엔드를 사용하는 ‘qt’로 변경하는 것을 고려하세요. ‘webengine’과 ‘qt’ 모두 mechanize가 지원하지 않는 HTTP/2를 지원하므로 봇 보호 서비스의 핑거프린팅이 더 어렵습니다.

center_navbar = True

True이면 탐색 모음이 가운데 정렬되고, 그렇지 않으면 왼쪽 정렬됩니다

compress_news_images = False

False로 설정하면 모든 크기 조정 및 압축 매개 변수를 무시하고 이미지를 수정 없이 통과시킵니다. True이고 다른 압축 매개 변수가 기본값으로 남겨져 있으면, 이미지가 출력 프로필로 설정된 화면 크기에 맞게 조정되고 최대 (w * h)/16 크기로 압축됩니다(w x h는 조정된 이미지 크기).

compress_news_images_auto_size = 16

JPEG 이미지를 자동 압축할 때 사용하는 계수입니다. None으로 설정하면 자동 압축이 비활성화됩니다. 그렇지 않으면, w x h가 픽셀 단위의 이미지 크기일 때 품질 수준을 낮춰 가능한 한 (w * h)/compress_news_images_auto_size 바이트로 크기를 줄입니다. 최소 JPEG 품질은 5/100이므로 이 제약 조건이 충족되지 않을 수 있습니다. 이 매개 변수는 이미지에 고정 최대 크기를 제공하는 compress_news_images_max_size 매개 변수로 재정의할 수 있습니다. scale_news_images_to_device를 활성화하면 이미지가 먼저 조정된 후 크기가 (w * h)/factor 미만이 될 때까지 품질이 낮아집니다(w와 h는 이제 조정된 이미지 크기). 즉, 이 압축은 크기 조정 후에 발생합니다.

compress_news_images_max_size = None

JPEG 품질을 설정하여 이미지가 주어진 크기(KBytes)를 초과하지 않도록 합니다. 설정하면 이 매개 변수가 compress_news_images_auto_size를 통한 자동 압축을 재정의합니다. 최소 JPEG 품질은 5/100이므로 이 제약 조건이 충족되지 않을 수 있습니다.

conversion_options = {}

다운로드된 콘텐츠를 전자책으로 변환하는 것을 제어하는 레시피별 옵션입니다. 사용자나 플러그인 지정 값을 재정의하므로 절대적으로 필요한 경우에만 사용하세요. 예:

conversion_options = {
'base_font_size' : 16,
'linearize_tables' : True,
}

cover_margins = (0, 0, '#ffffff')

기본적으로 get_cover_url()이 반환하는 표지 이미지가 정기간행물의 표지로 사용됩니다. 레시피에서 이 메서드를 덮어쓰면 calibre가 다운로드된 표지를 다운로드된 표지의 너비와 높이의 백분율로 표현되는 프레임에 렌더링하도록 지시합니다. cover_margins = (10, 15, ‘#ffffff’)는 표지에 왼쪽과 오른쪽에 10px, 위쪽과 아래쪽에 15px의 흰색 여백을 추가합니다. 색상 이름은 `여기 <https://www.imagemagick.org/script/color.php>`_에 정의되어 있습니다. 어떤 이유에서인지 Windows에서 white가 항상 작동하지 않는 점에 유의하세요. 대신 #ffffff를 사용하세요

delay = 0

연속 다운로드 사이의 기본 지연 시간(초)입니다. 인수는 더 정확한 시간을 나타내기 위해 부동소수점 숫자일 수 있습니다. URL별 지연 시간을 구현하려면 :meth:`get_url_specific_delay`를 참조하세요.

description = ''

이 레시피가 다운로드하는 콘텐츠를 설명하는 몇 줄입니다. 주로 레시피 목록을 표시하는 GUI에서 사용됩니다.

encoding = None

잘못된 문자 집합 명세를 가진 사이트에 대한 인코딩 재정의를 지정하세요. 가장 일반적인 것은 ``latin1``을 지정하고 ``cp1252``를 사용하는 것입니다. None이면 인코딩을 감지하려고 시도합니다. 호출 가능 객체이면 레시피 객체와 디코딩할 소스의 두 인수로 호출됩니다. 디코딩된 소스를 반환해야 합니다.

extra_css = None

다운로드된 HTML 파일에 추가해야 할 추가 CSS`를 지정하세요. 닫는 ` 태그 바로 앞의 <style> 태그에 삽입되어 개별 HTML 태그의 style 속성을 사용하여 선언된 것을 제외한 모든 CSS`를 재정의합니다. 프로그래밍 방식으로 extra_css를 생성하려면 대신 :meth:`get_extra_css() 메서드를 덮어쓰세요. 예:

extra_css = '.heading { font: serif x-large }'

feeds = None

다운로드할 피드 목록입니다. [url1, url2, ...] 또는 ``[(‘제목1’, url1), (‘제목2’, url2),…]``일 수 있습니다

filter_regexps = []

무시할 링크를 결정하는 정규 표현식 목록입니다. 비어 있으면 무시됩니다. is_link_wanted가 구현되지 않은 경우에만 사용됩니다. 예:

filter_regexps = [r'ads\.doubleclick\.net']

`ads.doubleclick.net`이 포함된 모든 URL을 제거합니다.

BasicNewsRecipe.match_regexps 또는 BasicNewsRecipe.filter_regexps 중 하나만 정의해야 합니다.

handle_gzip = True

mechanize 브라우저에서 gzipped 전송을 사용하지 않으려면 False로 설정하세요. 일부 오래된 서버는 gzip에서 문제가 발생할 수 있습니다.

ignore_duplicate_articles = None

둘 이상의 섹션에 존재하는 기사의 중복을 무시합니다. 중복 기사는 동일한 제목 및/또는 URL을 가진 기사입니다. 동일한 제목의 기사를 무시하려면 다음으로 설정하세요:

ignore_duplicate_articles = {'title'}

대신 URL을 사용하려면 다음으로 설정하세요:

ignore_duplicate_articles = {'url'}

제목 또는 URL로 일치시키려면 다음으로 설정하세요:

ignore_duplicate_articles = {'title', 'url'}

keep_only_tags = []

지정된 태그와 그 자식만 유지합니다. 태그 지정 형식은 BasicNewsRecipe.remove_tags`를 참조하세요. 이 목록이 비어 있지 않으면 ` 태그가 비워지고 이 목록의 항목과 일치하는 태그로 다시 채워집니다. 예:

keep_only_tags = [dict(id=['content', 'heading'])]

id 속성이 “content” 또는 `”heading”`인 태그만 유지합니다.

language = 'und'

뉴스의 언어입니다. 두 글자 또는 세 글자 길이의 ISO-639 코드여야 합니다

masthead_url = None

기본적으로 calibre는 마스트헤드에 기본 이미지를 사용합니다(Kindle 전용). 마스트헤드로 사용할 URL을 제공하려면 레시피에서 이 값을 재정의하세요.

match_regexps = []

따라갈 링크를 결정하는 정규 표현식 목록입니다. 비어 있으면 무시됩니다. is_link_wanted가 구현되지 않은 경우에만 사용됩니다. 예:

match_regexps = [r'page=[0-9]+']

`page=숫자`가 포함된 모든 URL과 일치합니다.

BasicNewsRecipe.match_regexps 또는 BasicNewsRecipe.filter_regexps 중 하나만 정의해야 합니다.

max_articles_per_feed = 100

각 피드에서 다운로드할 최대 기사 수입니다. 주로 기사 날짜가 없는 피드에 유용합니다. 대부분의 피드에서는 :attr:`BasicNewsRecipe.oldest_article`을 사용하세요

needs_subscription = False

True이면 GUI가 다운로드 중 사용할 사용자 이름과 비밀번호를 사용자에게 묻습니다. “optional”로 설정하면 사용자 이름과 비밀번호 사용이 선택 사항이 됩니다.

no_stylesheets = False

전자책 형식으로 변환하기에 부적합할 정도로 스타일시트가 지나치게 복잡한 웹사이트에서 스타일시트 로드를 비활성화하는 편의 플래그입니다. True이면 스타일시트를 다운로드하거나 처리하지 않습니다.

oldest_article = 7.0

이 뉴스 소스에서 다운로드할 가장 오래된 기사. 일 단위.

preprocess_regexps = []

다운로드된 HTML`에 대해 실행할 :term:`regexp 치환 규칙 목록입니다. 목록의 각 요소는 2요소 튜플이어야 합니다. 튜플의 첫 번째 요소는 컴파일된 정규 표현식이고, 두 번째 요소는 단일 일치 객체를 받아 일치를 대체할 문자열을 반환하는 호출 가능 객체입니다. 예:

preprocess_regexps = [
(re.compile(r'<!--Article ends here-->.*</body>', re.DOTALL|re.IGNORECASE),
lambda match: '</body>'),
]

`<!–Article ends here–>`부터 `</body>`까지 모든 것을 제거합니다.

publication_type = 'unknown'

발행물 유형입니다. newspaper, magazine 또는 blog로 설정합니다. None으로 설정하면 발행물 유형 메타데이터가 opf 파일에 기록되지 않습니다.

recipe_disabled = None

비어 있지 않은 문자열로 설정하면 이 레시피가 비활성화됩니다. 해당 문자열이 비활성화 메시지로 사용됩니다

recipe_specific_options = None

이 레시피에 특화된 옵션을 지정하세요. 뉴스 가져오기 대화상자의 고급 탭이나 ebook-convert 명령줄에서 사용자가 맞춤 설정할 수 있습니다. 옵션은 옵션 이름을 옵션에 대한 메타데이터에 매핑하는 딕셔너리로 지정됩니다. 예:

recipe_specific_options = {
'edition_date': {
'short': 'The issue date to download',
'long': 'Specify a date in the format YYYY-mm-dd to download the issue corresponding to that date',
'default': 'current',
}
}

레시피가 실행되면 self.recipe_specific_options는 옵션 이름을 사용자가 지정한 옵션 값에 매핑하는 dict가 됩니다. 사용자가 옵션을 지정하지 않으면 ‘default’에 지정된 값을 갖습니다. 기본값이 지정되지 않은 경우 사용자가 해당 옵션을 지정하지 않으면 그 옵션은 dict에 아예 포함되지 않습니다.

recursions = 0

기사 웹페이지에서 따라갈 링크의 단계 수입니다.

remove_attributes = []

모든 태그에서 제거할 속성 목록입니다. 예:

remove_attributes = ['style', 'font']

remove_empty_feeds = False

True이면 비어 있는 피드를 출력에서 제거합니다. 이 옵션은 하위 클래스에서 parse_index를 재정의한 경우에는 효과가 없습니다. feeds 또는 get_feeds를 사용해 피드 목록을 반환하는 레시피에서만 사용됩니다. ignore_duplicate_articles 옵션을 사용할 때도 함께 사용됩니다.

remove_javascript = True

다운로드된 HTML에서 모든 JavaScript 태그를 제거하는 편리한 플래그

remove_tags = []

제거할 태그 목록입니다. 지정된 태그가 다운로드된 HTML에서 제거됩니다. 태그는 다음 형식의 딕셔너리로 지정됩니다:

{
name : 'tag name', #e.g. 'div'
attrs : a dictionary, #e.g. {'class': 'advertisement'}
}

모든 키는 선택 사항입니다. 검색 기준에 대한 전체 설명은 `Beautiful Soup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/#searching-the-tree>`__을 참조하세요. 일반적인 예:

remove_tags = [dict(name='div', class_='advert')]

다운로드된 HTML`에서 모든 ` 태그와 그 자식을 제거합니다.

remove_tags_after = None

지정된 태그 이후에 오는 모든 태그를 제거합니다. 태그 지정 형식은 :attr:`BasicNewsRecipe.remove_tags`를 참조하세요. 예:

remove_tags_after = [dict(id='content')]

`id=”content”`인 첫 번째 요소 이후의 모든 태그를 제거합니다.

remove_tags_before = None

지정된 태그 이전에 오는 모든 태그를 제거합니다. 태그 지정 형식은 :attr:`BasicNewsRecipe.remove_tags`를 참조하세요. 예:

remove_tags_before = dict(id='content')

`id=”content”`인 첫 번째 요소 이전의 모든 태그를 제거합니다.

requires_version = (0, 6, 0)

이 레시피를 사용하는 데 필요한 최소 calibre 버전

resolve_internal_links = False

True로 설정하면 다운로드된 기사 안에서 다른 다운로드된 기사를 가리키는 링크가 원래 웹 URL이 아니라 다운로드된 기사 사본을 가리키도록 변경됩니다. 이를 True로 설정하면 사용 중인 웹사이트의 URL 체계에 맞게 canonicalize_internal_url도 구현해야 할 수 있습니다.

reverse_article_order = False

각 피드에서 기사 순서를 반대로 합니다.

scale_news_images = None

이미지를 조정할 최대 크기(w,h)입니다. scale_news_images_to_device가 True이면 출력 프로필로 설정된 장치 화면 크기로 설정됩니다. 프로필이 설정되지 않은 경우 할당된 값(기본값 None)으로 유지됩니다.

scale_news_images_to_device = True

출력 프로필로 설정된 장치 화면 크기에 맞게 이미지 크기를 다시 조정합니다. 출력 프로필이 설정되지 않은 경우 무시됩니다.

simultaneous_downloads = 5

동시 다운로드 수입니다. 서버가 까다로우면 1로 설정하세요. BasicNewsRecipe.delay > 0이면 자동으로 1로 줄어듭니다

summary_length = 500

짧은 설명에 사용할 최대 문자 수입니다.

template_css = '\n            .article_date {\n                color: gray; font-family: monospace;\n            }\n\n            .article_description {\n                text-indent: 0pt;\n            }\n\n            a.article {\n                font-weight: bold; text-align:left;\n            }\n\n            a.feed {\n                font-weight: bold;\n            }\n\n            .calibre_navbar {\n                font-family:monospace;\n            }\n    '

템플릿, 즉 내비게이션 바와 목차의 스타일을 지정하는 데 사용되는 CSS입니다. 이 변수를 재정의하기보다는 레시피에서 extra_css를 사용해 모양과 느낌을 사용자 지정해야 합니다.

timefmt = ' [%a, %d %b %Y]'

첫 페이지에 표시되는 날짜의 형식 문자열입니다. 기본값: 요일_이름, 일_번호 월_이름 연도

timeout = 120.0

서버에서 파일을 가져올 때의 제한 시간(초)입니다.

title = '알 수 없는 뉴스 출처'

전자책에 사용할 제목입니다.

use_embedded_content = None

보통은 포함된 콘텐츠의 길이를 기준으로 피드 안에 전체 기사가 내장되어 있는지 추정합니다. None이면 기본 추정 방식을 사용합니다. True이면 피드에 내장 콘텐츠가 항상 있다고 가정하고, False이면 피드에 내장 콘텐츠가 없다고 항상 가정합니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 전자책 뷰어

전자책 뷰어
calibre에는 주요 전자책 형식을 모두 볼 수 있는 내장 전자책 뷰어가 포함되어 있습니다. 전자책 뷰어는 다양한 방식으로 사용자 지정할 수 있으며 고급 기능도 많이 제공합니다.

- 전자책 뷰어 시작하기

- 전자책 탐색하기

- 강조표시 텍스트

- 소리내어 읽기

- 텍스트 검색

- 키보드만으로 링크 따라가기

- 사용자의 독서 체험의 모양새 사용자 정의

- 사전 조회

- 텍스트 및 이미지 복사중

- 이미지 확대

- 현재 책의 종이책 판본과 동기화

- 키보드 단축키

- 재흐름이 불가능한 콘텐츠

- calibre 전자책 뷰어에서 잘 작동하도록 책 설계하기

전자책 뷰어 시작하기
calibre 라이브러리에서 책을 선택한 다음 보기 버튼을 누르면 해당 책을 볼 수 있습니다. 그러면 책이 전자책 뷰어에서 열립니다. Windows에서는 시작 메뉴에서 전자책 뷰어만 따로 실행할 수도 있습니다. macOS에서는 Dock에 고정한 뒤 거기서 실행할 수 있습니다. Linux에서는 데스크톱 메뉴의 실행기를 사용하거나 ebook-viewer 명령을 실행하면 됩니다.

전자책 탐색하기

책에서 “페이지를 넘기려면” 다음 방법 중 하나를 사용하면 됩니다:

- 마우스로 왼쪽 또는 오른쪽 여백이나 페이지를 클릭

- 스페이스바, Page Up, Page Down 또는 화살표 키를 누름

- 터치스크린에서는 텍스트를 탭하거나 좌우로 스와이프

다음 방법 중 하나로 뷰어 제어창을 열 수 있습니다:

- 텍스트를 마우스 오른쪽 버튼으로 클릭

- Esc 또는 Menu 키를 누름

- 터치스크린에서는 화면 위쪽 1/3을 탭

뷰어에는 “페이지 모드”와 “흐름 모드”의 두 가지 모드가 있습니다. 페이지 모드에서는 종이책처럼 책 내용이 페이지 단위로 표시됩니다. 흐름 모드에서는 웹 브라우저처럼 텍스트가 연속적으로 표시됩니다. 뷰어 환경설정`의 :guilabel:`페이지 레이아웃`에서 둘 사이를 전환하거나 :kbd:`Ctrl+M 키를 눌러 전환할 수 있습니다.

책갈피

책을 읽다가 중간에 전자책 뷰어를 닫으면, 다음에 그 책을 열 때 마지막으로 읽던 위치를 기억해 그곳으로 돌아갑니다. 전자책 뷰어 제어창의 책갈피 버튼을 사용하거나 Ctrl+B`를 눌러 책갈피를 설정할 수도 있습니다. EPUB 형식의 책을 볼 때 이 책갈피는 실제로 EPUB 파일 자체에 저장됩니다. 책갈피를 추가한 뒤 파일을 친구에게 보내면, 친구가 그 파일을 열었을 때 당신의 책갈피를 볼 수 있습니다. 이 동작은 뷰어 환경설정의 :guilabel:`기타 섹션에서 끌 수 있습니다.

목차

읽고 있는 책에 목차가 정의되어 있다면 목차 버튼을 눌러 열 수 있습니다. 그러면 책의 섹션 목록이 표시됩니다. 원하는 항목을 클릭하면 해당 부분으로 바로 이동할 수 있습니다.

위치로 이동하기

전자책은 종이책과 달리 페이지라는 개념이 없습니다. 뷰어 제어창의 이동 → 위치 기능을 사용하면 전자책의 정확한 위치를 지정할 수 있습니다.

이 위치 정보는 친구와 책에 대해 이야기하거나 다른 글에서 책의 특정 부분을 언급할 때 모호함 없이 사용할 수 있습니다. 이 위치는 뷰어 제어창의 :guilabel:`이동->위치`에서 입력할 수 있습니다.

클립보드에 복사해 다른 프로그램이나 문서에 붙여 넣을 수 있는 URL도 있습니다. 이 URL을 클릭하면 calibre 전자책 뷰어가 현재 위치에서 책을 엽니다.

전자책 안의 링크를 클릭해 미주 같은 책의 다른 부분으로 이동했다면, 뷰어 제어창 왼쪽 위에 있는 뒤로 및 앞으로 버튼을 사용할 수 있습니다. 이 버튼은 웹 브라우저의 같은 버튼처럼 동작합니다.

참조 모드
calibre에는 매우 유용한 참조 모드`도 있습니다. 뷰어 제어창에서 :guilabel:`참조 모드 버튼을 클릭하면 켤 수 있습니다. 이 모드를 켜면 각 문단 시작 부분에 섹션 번호와 문단 번호로 이루어진 고유 번호가 표시됩니다.

이 번호를 사용하면 친구와 책에 대해 이야기하거나 다른 글에서 책의 특정 부분을 언급할 때 모호함 없이 지목할 수 있습니다. :guilabel:`이동 기능`에 이 번호를 입력하면 해당 참조 위치로 이동할 수 있습니다.

강조표시 텍스트

뷰어에서 텍스트를 선택하면 선택 영역 옆에 작은 팝업 막대가 나타납니다. 그 막대의 강조 표시 버튼을 클릭해 하이라이트를 만들 수 있습니다. 메모를 추가하거나 하이라이트 색상을 바꿀 수도 있습니다. 터치스크린에서는 단어를 길게 탭해 선택하고 팝업 막대를 표시합니다. 하이라이트 모드에 들어가면 터치스크린에 맞는 선택 핸들을 사용해 선택된 텍스트를 바꿀 수 있습니다. 선택하는 동안 스크롤하려면 핸들을 위쪽 또는 아래쪽 여백으로 끌면 됩니다. 특히 여러 페이지에 걸친 선택에는 :kbd:`Shift+클릭`이나 :kbd:`오른쪽 클릭`으로 선택 범위를 확장하는 방법이 유용합니다.

뷰어 제어창의 하이라이트 버튼을 사용하면 장별로 정렬된 책의 모든 하이라이트 목록을 별도 패널에 표시할 수 있습니다.

보기 버튼을 마우스 오른쪽 버튼으로 클릭한 다음 :guilabel:`주석 찾아보기`를 선택하면 calibre 라이브러리 전체의 *모든 하이라이트*를 탐색할 수 있습니다.

마지막으로 calibre 콘텐츠 서버의 브라우저 내장 뷰어를 사용한다면, 뷰어 환경설정의 :guilabel:`환경설정->기타`로 가서 동기화할 콘텐츠 서버 뷰어의 사용자 이름을 입력하면 주석을 브라우저 뷰어와 동기화할 수 있습니다. 익명 사용자와 동기화하려면 특수 값 ``*``를 사용하십시오.

소리내어 읽기

뷰어는 책의 텍스트를 소리 내어 읽을 수 있습니다. 사용하려면 뷰어 제어창의 소리 내어 읽기 버튼을 클릭하면 됩니다. 현재 읽고 있는 단어 또는 문장은 강조 표시됩니다. 음성은 Piper [https://github.com/rhasspy/piper] 신경망 텍스트 음성 변환 엔진이나 운영 체제의 텍스트 음성 변환 서비스를 사용해 텍스트로부터 합성됩니다. :guilabel:`소리 내어 읽기`가 활성화되어 있을 때 표시되는 막대의 톱니바퀴 아이콘을 클릭하면 사용하는 백엔드와 음성을 바꿀 수 있습니다.

뷰어 환경설정의 선택 동작`에서 선택 막대에 :guilabel:`소리 내어 읽기 버튼을 추가하면 강조 표시한 구절도 소리 내어 읽을 수 있습니다.

참고

브라우저의 텍스트 음성 변환 지원은 매우 불완전하고 버그도 많기 때문에, 브라우저 내장 뷰어에서 :guilabel:`소리 내어 읽기`가 얼마나 잘 동작할지는 사용하는 브라우저의 텍스트 음성 변환 지원 수준에 따라 달라집니다.

텍스트 검색

뷰어에는 매우 강력한 검색 기능이 있습니다. Ctrl+F 키를 누르거나 뷰어 제어창을 열고 검색을 클릭하십시오. 가장 간단한 검색은 입력 상자에 넣은 텍스트를 그대로 찾는 것입니다. 검색 입력 아래의 검색 모드 상자에서 다양한 검색 방식을 선택할 수 있습니다. 사용 가능한 모드는 다음과 같습니다:

1. 포함 - 가장 단순한 기본 모드입니다. 검색 상자에 입력한 텍스트를 어디서나 검색합니다. 모든 문장 부호, 악센트, 공백은 무시됩니다. 예를 들어 ``Pena``를 검색하면 ``penal, pen a, pen.a, Peña``가 모두 일치합니다. 대/소문자 구분 상자를 선택하면 악센트, 공백, 문장 부호를 더 이상 무시하지 않습니다.

2. 전체 단어 - 완전한 단어만 검색합니다. 예를 들어 ``pena``를 검색하면 ``Peña``에는 일치하지만 ``Penal``에는 일치하지 않습니다. 위의 포함 검색과 마찬가지로 대/소문자 구분 상자를 선택하지 않으면 악센트와 문장 부호는 무시됩니다.

3. 인접 단어 - 서로 가까이 있는 전체 단어를 검색합니다. 예를 들어 ``calibre cool``을 검색하면 ``calibre``와 ``cool``이라는 단어가 서로 60자 이내에 나타나는 위치와 일치합니다. 문자 수를 바꾸려면 단어 목록 끝에 새 숫자를 추가하십시오. 예를 들어 ``calibre cool awesome 120``은 세 단어가 서로 120자 이내에 나타나는 위치와 일치합니다. 이 검색에서는 문장 부호와 악센트를 무시하지 않습니다.

4. 정규식 - 검색 텍스트를 *정규 표현식*으로 해석합니다. 정규 표현식 사용법에 대해 더 알아보려면 :doc:`튜토리얼 <regexp>`을 참조하십시오.

키보드만으로 링크 따라가기

전자책 뷰어에는 마우스를 사용하지 않고 텍스트 안의 링크를 클릭할 수 있게 해 주는 힌트 모드`가 있습니다. :kbd:`Alt+F 키를 누르면 현재 화면의 모든 링크 위에 숫자나 글자가 표시되어 강조됩니다. 키보드에서 해당 글자를 누르면 링크를 클릭할 수 있습니다. Esc 키를 누르면 어떤 링크도 선택하지 않고 :guilabel:`힌트 모드`를 종료합니다.

화면에 35개가 넘는 링크가 있으면 일부 링크에는 여러 글자가 할당됩니다. 이 경우 첫 번째와 두 번째 글자를 차례로 입력하거나, 첫 번째 글자를 입력한 뒤 Enter`를 눌러 활성화할 수 있습니다. 입력 실수는 :kbd:`Backspace 키로 되돌릴 수도 있습니다.

사용자의 독서 체험의 모양새 사용자 정의

뷰어 제어창의 글꼴 크기`를 사용하거나 :kbd:`Ctrl++, Ctrl+-`를 누르거나 :kbd:`Ctrl 키를 누른 채 마우스 휠을 움직여 글꼴 크기를 즉시 바꿀 수 있습니다.

색상은 뷰어 환경설정의 색상 섹션에서 바꿀 수 있습니다.

뷰어 환경설정의 :guilabel:`페이지 레이아웃`에서 화면에 표시할 페이지 수와 페이지 여백을 바꿀 수 있습니다.

뷰어 환경설정의 머리글과 바닥글 섹션에서 남은 읽기 시간, 현재 장 제목, 책 위치 같은 사용자 정의 머리글과 바닥글을 표시할 수 있습니다.

:guilabel:’스타일’ 설정을 통해 고급 사용자 정의를 수행할 수 있습니다. 여기에서 텍스트 아래에 표시할 배경 이미지와 모든 책에 적용할 스타일시트를 지정할 수 있습니다. 그것을 사용하여 단락 스타일 변경, 텍스트 정렬 등과 같은 작업을 수행할 수 있습니다.캘리버 사용자가 사용하는 사용자 정의 스타일시트의 예는 `포럼 <https://www.mobileread.com/forums/showthread.php?t=51500>`_을 참조하십시오.

사전 조회

현재 책에서 단어의 뜻을 찾으려면 조회할 단어를 두 번 클릭하거나 길게 탭한 다음, 라이브러리 모양의 조회 버튼을 클릭하십시오.

텍스트 및 이미지 복사중

마우스로 내용을 끌어 텍스트와 이미지를 선택한 뒤 마우스 오른쪽 버튼을 눌러 :guilabel:`복사`를 선택하면 클립보드로 복사할 수 있습니다. 복사한 자료는 다른 응용 프로그램에 일반 텍스트와 이미지로 붙여 넣을 수 있습니다.

이미지 확대

이미지를 두 번 클릭하거나 길게 탭하면 별도 창에서 원래 크기로 확대해 볼 수 있습니다. 이미지를 마우스 오른쪽 버튼으로 클릭한 다음 :guilabel:`이미지 보기`를 선택해도 됩니다.

현재 책의 종이책 판본과 동기화

인쇄판이 함께 있는 일부 전자책에는 종이책 각 페이지의 시작 위치를 표시하는 메타데이터가 포함되어 있습니다. 이런 전자책의 경우 뷰어 제어창의 이동 버튼을 사용해 특정 종이책 페이지로 이동할 수 있습니다. 또한 뷰어 설정에서 머리글이나 바닥글에 :guilabel:`종이책 페이지`를 추가하면, 현재 위치에 해당하는 종이책 페이지를 책의 머리글이나 바닥글에 표시할 수도 있습니다.

키보드 단축키

뷰어에는 calibre의 다른 부분과 마찬가지로 다양한 키보드 단축키가 있습니다. 이 단축키는 뷰어 :guilabel:`환경설정`에서 사용자 지정할 수 있습니다. 기본 단축키는 아래와 같습니다:

calibre 전자책 뷰어용 키보드 단축키

키보드 단축키

동작

Home, Ctrl+ArrowUp, Ctrl+ArrowLeft

여러 파일로 이루어진 책에서 현재 파일의 시작 부분으로 스크롤

Ctrl+Home

이 책의 처음으로 스크롤

Ctrl+End

이 책의 마지막으로 스크롤

End, Ctrl+ArrowDown, Ctrl+ArrowRight

여러 파일로 이루어진 책에서 현재 파일의 끝으로 스크롤

ArrowUp

흐름 모드에서는 부드럽게 뒤로 스크롤하고, 페이지 모드에서는 한 화면씩 뒤로 이동

ArrowDown

앞으로 스크롤 (연속 보기 모드에서는 부드럽게, 페이지 단위로 보기 모드에서는 화면 단위로)

ArrowLeft

흐름 모드에서는 왼쪽으로 조금 스크롤하고, 페이지 모드에서는 한 페이지 왼쪽으로 이동

ArrowRight

흐름 모드에서는 오른쪽으로 조금 스크롤하고, 페이지 모드에서는 한 페이지 오른쪽으로 이동

PageUp, Shift+Spacebar

화면 단위로 뒤로 스크롤

PageDown, Spacebar

화면 단위로 앞으로 스크롤

Ctrl+PageUp

이전 구간으로 스크롤

Ctrl+PageDown

다음 구간으로 스크롤

Alt+ArrowLeft

뒤로 가기

Alt+ArrowRight

앞으로 가기

Ctrl+T

목차 표시 전환

Ctrl+S

소리내어 읽기

Alt+P

:guilabel:`프로필`을 만들어 전환하면서 설정을 빠르게 변경

Alt+f

키보드로 링크 따라가기

Ctrl+C

클립보드로 복사

Alt+C

클립보드에 현재 위치 복사

Ctrl+Shift+C

현재 위치를 클립보드에 calibre:// URL로 복사

/, Ctrl+f, Cmd+f

검색 시작

F3, Enter

다음 찾기

Shift+F3, Shift+Enter

이전 찾기

Ctrl+Plus, Meta+Plus

글꼴 크기 증가

Ctrl+Minus, Meta+Minus

글꼴 크기 감소

Ctrl+0

기본 글꼴 크기 복원

Ctrl+]

화면당 페이지 수 늘이기

Ctrl+[

화면당 페이지 수 줄이기

Ctrl+Alt+C

화면당 페이지 수를 자동으로 계산하기

F11, Ctrl+Shift+F

전체화면 모드 전환

Ctrl+M

연속 보기 모드/페이지 단위로 보기 모드 전환

Ctrl+W

스크롤바 전환

Ctrl+X

참조 모드 켜고 끄기

Ctrl+B

책갈피 패널 표시/숨김

Ctrl+Alt+B

책갈피 추가하기

Ctrl+N, Ctrl+E

메타데이터 표시

Ctrl+Alt+F5, Ctrl+Alt+R

책 다시 읽어오기

Ctrl+Shift+ArrowRight

현재 선택 범위를 한 단어 앞으로 확장

Ctrl+Shift+ArrowLeft

현재 선택 범위를 한 단어 뒤로 확장

Shift+ArrowRight

현재 선택 범위를 한 글자 앞으로 확장

Shift+ArrowLeft

현재 선택 범위를 한 글자 뒤로 확장

Shift+ArrowDown

현재 선택 범위를 한 줄 앞으로 확장

Shift+Home

현재 선택 범위를 줄의 시작까지 확장

Shift+End

현재 선택 범위를 줄의 끝까지 확장

Ctrl+A

모두 선택하기

Shift+ArrowUp

현재 선택 범위를 한 줄 뒤로 확장

Ctrl+Shift+ArrowDown

현재 선택 범위를 한 문단 앞으로 확장

Ctrl+Shift+ArrowUp

현재 선택 범위를 한 문단 뒤로 확장

Esc, MenuKey

전자책 뷰어 제어창 표시

Ctrl+Comma, Ctrl+Esc, Meta+Esc, Meta+Comma

전자책 뷰어 환경설정 표시

Ctrl+G, ;, :

지정한 책 위치 또는 지점으로 이동

Ctrl+Spacebar

자동 스크롤 켜고 끄기

Alt+ArrowUp

자동 스크롤 빠르게

Alt+ArrowDown

자동 스크롤 속도 줄이기

Ctrl+I

들여다보기 도구 표시/숨김

Ctrl+L

단어 찾아보기 패널 표시/숨김

Ctrl+Q (macOS에서는 Cmd+Q)

종료

Ctrl+P

PDF 파일로 책 인쇄

Ctrl+F11

도구모음 전환

Ctrl+H

하이라이트 패널 켜고 끄기

Ctrl+D

책 편집하기

재흐름이 불가능한 콘텐츠

일부 책에는 페이지 경계에서 나눌 수 없는 매우 넓은 콘텐츠가 있습니다. 예를 들어 표나 <pre> 태그가 그렇습니다. 이런 경우에는 Ctrl+M`을 눌러 뷰어를 *흐름 모드*로 전환한 뒤 이 콘텐츠를 읽어야 합니다. 또는 뷰어 환경설정의 :guilabel:`스타일 섹션에 다음 CSS를 추가해 <pre> 태그 안의 텍스트 줄을 강제로 줄바꿈하게 할 수도 있습니다:

code, pre { white-space: pre-wrap }

calibre 전자책 뷰어에서 잘 작동하도록 책 설계하기
calibre 전자책 뷰어는 루트 요소에 is-calibre-viewer 클래스를 설정합니다. 따라서 이 뷰어에만 적용되는 CSS 규칙을 작성할 수 있습니다. 또한 뷰어는 body 요소에 다음 클래스를 설정합니다:

body.calibre-viewer-dark-colors

어두운 색상표를 사용할 때 지정

body.calibre-viewer-light-colors

밝은 색상표를 사용할 때 지정

body.calibre-viewer-paginated

페이지 모드일 때 설정됨

body.calibre-viewer-scrolling

흐름 모드(비페이지 모드)일 때 설정됨

body.calibre-footnote-container

팝업 각주를 표시할 때 설정됨

마지막으로 CSS 변수 <https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties>`_를 통해 calibre 색상 구성표의 색상을 사용할 수 있습니다. calibre 전자책 뷰어는 `–calibre-viewer-background-color``, --calibre-viewer-foreground-color 변수를 정의하며, 링크 색상을 정의한 색상 테마에서는 선택적으로 ``–calibre-viewer-link-color``도 정의합니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 전자책 변환

전자책 변환

캘리버는 매우 사용하기 쉽도록 설계된 변환 시스템을 가지고 있습니다. 보통은, 사용자가 캘리버에 책을 추가하고 변환을 클릭하기만 하면, 입력된 책에 가장 가까운 결과물을 만들기 위하여 캘리버가 최선을 다하게 됩니다. 하지만, 캘리버가 지원하는 매우 다양한 종류의 입력 형식들이 모두 전자책 변환에 적합한 것은 아닙니다. 만일 입력 형식이 이러한 경우이거나 또는 사용자가 변환 시스템에 더 많은 제어를 원하는 경우, 캘리버에는 변환 과정을 세부적으로 조정할 수 있는 아주 많은 옵션들이 있습니다. 하지만, 캘리버의 변환 시스템이 전용 전자책 편집기를 대체할 수 없다는 점은 항상 명심하시기 바랍니다. 전자책을 편집하는 방법으로는, 우선 캘리버를 사용하여 책을 EPUB이나 AZW3 형식으로 변환한 뒤, 책 편집 특정 기능을 사용하여 완벽한 형태로 다듬는 것을 추천합니다. 그런 다음 편집이 완료된 전자책을 입력으로 사용하여 캘리버에서 다른 형식들로 변환할 수 있습니다.

이 문서는 주로 아래 사진과 같이 변환 대화창에 있는 변환 설정을 참조합니다. 이러한 모든 설정은 명령줄 인터페이스를 통해 변환할 수도 있으며, 문서:doc:`generated/ko/ebook-convert`에 설명되어 있습니다. 캘리버에서 개별 설정에 대한 도움말을 얻을 수 있습니다. 마우스를 가져가면 설정을 설명하는 도구 설명이 나타납니다.

목차

- 소개

- 모양새

- 페이지 설정

- 합리적 추측에 의한(Heuristic) 처리

- 검색 & 교체

- 구조 탐지

- 목차

- HTML 입력 문서를 변환할 때 이미지를 장 제목으로 사용하기

- 목차 항목의 텍스트를 태그 속성으로 제공하기

- 변환 옵션의 설정/저장 방식

- 형식별 팁

소개

가장 먼저 이해하여야 할 점은, 변환 시스템이 파이프라인(한 단계의 출력이 다음 단계의 입력이 이어지는 형태)으로 설계되었다는 점입니다. 도식적으로는 아래와 같습니다:

입력 형식(Input Format)은 우선 적절한 *입력 플러그인(Input Plugin)*에 의해 XHTML로 변환됩니다. 그런 다음 이 HTML이 *변형(Transformed)*됩니다. 마지막 단계에서 이 처리된 XHTML이 적절한 *출력 플러그인(Ouput Plugin)*에 의해 지정된 출력 형식(Output Format)으로 변환됩니다. 변환의 결과물을 입력 형식에 따라 큰 차이가 있을 수 있습니다. 몇몇 형식은 다른 형식들 보다 변환이 더 잘 이루어집니다. 변환용으로 가장 적절한 입력 형식 목록은 :ref:`이곳 <best-source-formats>`을 참조하세요.

작업의 대부분은 XHTML 출력에 대해 행해지는 변형 과정에서 일어납니다. 변형 과정에는 다양한 것들이 있는데, 예를 들어, 책 시작 부분에 메타데이터 페이지로 삽입하거나, 장(章) 제목을 감지하여 자동으로 목차를 만들고, 글꼴 크기를 비례적으로 조정하는 등 다양한 변환이 있습니다. 모든 변환은 입력 파일 자체가 아니라 *입력 플러그인*에 의해 XHTML 출력에 작용한다는 점을 기억해야 합니다. 예를 들어, RTF 파일을 EPUB로 변환하도록 cibre에 요청하면 먼저 내부적으로 XHTML로 변환되고 다양한 변환이 XHTML에 적용된 다음 *출력 플러그인*이 EPUB 파일을 생성하여 모든 메타데이터, 목차, 등등을 자동으로 생성합니다.

이 과정을 실제로 보려면 디버그 옵션 를 사용하면 된다. 디버그 출력용 폴더 경로만 지정하면 된다. 변환 중 calibre는 변환 파이프라인의 여러 단계에서 생성된 XHTML을 서로 다른 하위 폴더에 저장한다. 네 개의 하위 폴더는 다음과 같다:

변환 파이프라인의 단계

폴더

설명

input

입력 플러그인의 HTML 출력을 담고 있습니다. 입력 플러그인을 디버깅하려면 이걸 사용하세요.

parsed

입력 플러그인의 출력에 대한 전처리와 XHTML로의 변환의 결과물입니다. 구조 탐지를 디버깅하는데 사용하세요.

structure

CSS flattening과 글꼴 크기 변환 이전의 사후 구조 탐지. 글꼴 크기 변환과 CSS 변형을 디버깅하는데 사용하세요.

processed

전자책이 출력 플러그인에 넘겨지기 직전의 단계. 출력 플러그인을 디버깅하는데 사용하세요.

calibre가 변환하기 전에 입력 문서를 조금 수정하고 싶다면, 가장 좋은 방법은 input 하위 폴더의 파일을 편집한 다음 그것을 ZIP으로 묶고, 이후 변환에서 그 ZIP 파일을 입력 형식으로 사용하는 것이다. 이렇게 하려면 메타 정보 편집 대화상자에서 그 ZIP 파일을 책의 형식으로 추가한 뒤, 변환 대화상자의 왼쪽 위에서 입력 형식으로 ZIP을 선택하면 된다.

이 문서는 주로 중간 단계의 XHTML에 행해지는 다양한 변형 과정과 그것들을 제어하는 방법에 대해 다룹니다. 마지막에는 각 입력/출력 형식에만 적용되는 몇몇 팁들이 있습니다.

모양새

목차

- 글꼴

- 텍스트

- 배치

- 스타일

- 스타일 변형

- HTML 변환

이 옵션 그룹은 변환된 전자책의 모양새의 다양한 측면을 제어합니다.

글꼴

전자책 읽기의 가장 큰 장점 중 하나는 개인의 필요와 조명 환경에 맞게 글꼴 크기를 쉽게 조절할 수 있다는 점이다. calibre에는 입력 문서에 어떤 글꼴 크기가 지정되어 있든, 출력되는 모든 책의 글꼴 크기를 일관되게 맞춰 주는 정교한 알고리즘이 들어 있다.

문서의 기본 글꼴 크기는 그 문서에서 가장 흔하게 쓰이는 글꼴 크기, 즉 본문 대부분의 글자 크기를 뜻한다. :guilabel:`기본 글꼴 크기`를 지정하면 calibre는 문서 안의 모든 글꼴 크기를 비례적으로 자동 재조정하여, 가장 흔한 글꼴 크기가 지정한 기본 글꼴 크기가 되도록 하고 다른 글꼴 크기도 그에 맞게 조정한다. 기본 글꼴 크기를 크게 하면 문서의 글꼴도 커지고, 반대로 작게 하면 글꼴도 작아진다. 최상의 결과를 얻으려면 기본 글꼴 크기를 설정할 때 글꼴 크기 키도 함께 설정하는 것이 좋다.

보통 calibre는 선택한 출력 프로필에 맞는 기본 글꼴 크기를 자동으로 고른다(페이지 설정 참고). 하지만 기본값이 자신에게 맞지 않으면 여기에서 덮어쓸 수 있다.

글꼴 크기 키 옵션은 기본 글꼴이 아닌 다른 글꼴 크기를 어떻게 재조정할지 제어한다. 글꼴 재조정 알고리즘은 글꼴 크기 키를 사용하며, 이것은 단순히 쉼표로 구분한 글꼴 크기 목록이다. 글꼴 크기 키는 주어진 글꼴 크기가 기본 글꼴 크기에 비해 몇 “단계” 크거나 작은지를 calibre에 알려 준다. 핵심 개념은 문서 안의 글꼴 크기 수가 제한적이어야 한다는 것이다. 예를 들어 본문용 크기 하나, 여러 단계의 제목용 크기 몇 개, 위첨자/아래첨자와 각주용 크기 몇 개 정도가 있다. 글꼴 크기 키를 이용하면 calibre가 입력 문서의 다양한 글꼴 크기를 서로 다른 논리적 글꼴 크기에 대응하는 별도의 “구간”으로 분류할 수 있다.

예를 들어 보자. 지금 변환하려는 원본 문서가 시력이 매우 좋은 사람이 만든 것이라 기본 글꼴 크기가 8pt라고 하자. 즉 문서 본문 대부분은 8pt이고, 제목은 조금 더 큰 10pt와 12pt 정도이며, 각주는 6pt처럼 조금 더 작다는 뜻이다. 이제 다음 설정을 사용하면:

Base font size : 12pt
Font size key : 7, 8, 10, 12, 14, 16, 18, 20

출력 문서는 기본 글꼴 크기 12pt, 제목 14pt와 16pt, 각주 8pt가 된다. 이제 가장 큰 제목 크기를 좀 더 두드러지게 하고, 각주도 약간 더 크게 만들고 싶다고 하자. 그러려면 글꼴 키를 다음처럼 바꾸면 된다:

New font size key : 7, 9, 12, 14, 18, 20, 22

그러면 가장 큰 제목은 18pt가 되고, 각주는 9pt가 된다. 글꼴 크기 키 설정 옆의 작은 버튼을 눌러 접근할 수 있는 글꼴 재조정 마법사를 사용하면, 어떤 설정이 자신에게 가장 적합한지 시험해 볼 수 있다.

입력 문서의 글꼴 크기를 그대로 유지하고 싶다면, 여기에서 변환 시의 모든 글꼴 크기 재조정을 끌 수도 있다.

관련 설정으로 :guilabel:`줄 높이`가 있다. 줄 높이는 줄의 세로 높이를 제어한다. 기본값(줄 높이 0)에서는 줄 높이를 조작하지 않는다. 기본값이 아닌 값을 지정하면, 자체 줄 높이가 지정되지 않은 모든 위치에 그 줄 높이가 적용된다. 다만 이 기능은 다소 거친 도구이므로 신중하게 사용하는 것이 좋다. 입력의 일부 구간만 줄 높이를 조정하고 싶다면 :ref:`Extra CSS <extra-css>`를 사용하는 편이 낫다.

이 섹션에서는 참조된 글꼴을 책 안에 포함하도록 calibre에 지시할 수도 있다. 그러면 리더 장치에 그 글꼴이 설치되어 있지 않아도 해당 글꼴을 사용할 수 있다.

텍스트

텍스트는 양쪽 정렬이 될 수도 있고 그렇지 않을 수도 있습니다. 양쪽 정렬된 텍스트에는 부드러운 오른쪽 여백을 제공하기 위해 단어 사이에 추가 공백이 있습니다. 어떤 사람들은 양쪽 정렬된 텍스트를 선호하지만 다른 사람들은 그렇지 않습니다. 일반적으로 캘리버는 원본 문서의 자리 맞춤을 유지합니다. 재정의하려면 이 섹션에서 텍스트 자리 맞춤 옵션을 사용할 수 있습니다.

calibre에 :guilabel:`문장부호 다듬기`를 지시할 수도 있다. 그러면 일반 따옴표, 대시, 줄임표를 올바른 조판용 문자로 바꾼다. 이 알고리즘은 완벽하지 않으므로 결과를 검토하는 것이 좋다. 반대로 :guilabel:`다듬은 문장부호 되돌리기`도 사용할 수 있다.

마지막으로 :guilabel:`입력 문자 인코딩`이 있다. 오래된 문서는 문자 인코딩을 지정하지 않는 경우가 있다. 이런 문서를 변환하면 비영어 문자나 스마트 따옴표 같은 특수 문자가 깨질 수 있다. calibre는 원본 문서의 문자 인코딩을 자동 감지하려고 하지만 항상 성공하는 것은 아니다. 이 설정을 사용하면 특정 문자 인코딩을 가정하도록 강제할 수 있다. `cp1252`는 Windows 소프트웨어로 만든 문서에서 흔히 쓰이는 인코딩이다. 인코딩 문제에 대해서는 :ref:`char-encoding-faq`도 읽어 보기 바란다.

배치

일반적으로 XHTML의 문단은 문단 사이에 빈 줄이 있고 문단 첫 줄 들여쓰기는 없다. calibre에는 이를 제어하는 옵션이 몇 가지 있다. :guilabel:`문단 사이 간격 제거`는 모든 문단 사이의 간격이 없도록 강제로 맞추고, 각 문단의 시작을 표시하기 위해 텍스트 들여쓰기를 1.5em으로 설정한다(변경 가능). :guilabel:`빈 줄 삽입`은 반대로 각 문단 쌍 사이에 정확히 한 줄의 빈 줄이 있도록 보장한다. 이 두 옵션은 매우 포괄적으로 동작하여 모든 문단(기술적으로는 <p> 와 <div> 태그)의 간격을 제거하거나 삽입한다. 입력 파일이 얼마나 지저분하든 옵션 이름 그대로 동작하도록 하기 위해서다. 유일한 예외는 입력 파일이 문단 간격을 만들기 위해 강제 줄바꿈을 사용하는 경우다.

선택한 일부만 제외하고 대부분 문단의 간격을 없애고 싶다면, 이 옵션을 사용하지 말고 대신 다음 CSS 코드를 :ref:`Extra CSS <extra-css>`에 추가하라:

p, div { margin: 0pt; border: 0pt; text-indent: 1.5em }
.spacious { margin-bottom: 1em; text-indent: 0pt; }

그런 다음 원본 문서에서 간격이 필요한 문단에 class=”spacious”`를 지정하면 된다. 입력 문서가 HTML이 아니라면, 서두에서 설명한 Debug 옵션을 사용해 HTML을 얻어라(:file:`input 하위 폴더 사용).

또 다른 유용한 옵션은 :guilabel:`표 선형화`이다. 잘못 설계된 일부 문서는 페이지의 텍스트 배치를 제어하기 위해 표를 사용한다. 이런 문서를 변환하면 텍스트가 페이지 밖으로 흘러나가거나 여러 가지 이상 현상이 생기곤 한다. 이 옵션은 표 안의 내용을 추출해서 선형 방식으로 표시한다. 다만 이 옵션은 모든 표를 선형화하므로, 입력 문서가 표를 실제 표 형식 데이터 표시 같은 정당한 목적으로 사용하지 않는다고 확신할 때만 사용해야 한다.

스타일

추가 CSS 옵션을 사용하면 입력의 모든 HTML 파일에 적용할 임의의 CSS를 지정할 수 있다. 이 CSS는 매우 높은 우선순위로 적용되므로 입력 문서 자체에 들어 있는 대부분의 CSS를 덮어쓴다. 이 설정을 사용하면 문서의 표시/배치를 세밀하게 조정할 수 있다. 예를 들어 endnote 클래스를 가진 모든 문단을 오른쪽 정렬하고 싶다면 다음을 추가하면 된다:

.endnote { text-align: right }

또는 모든 문단의 들여쓰기를 바꾸고 싶다면:

p { text-indent: 5mm; }

:guilabel:`추가 CSS`는 매우 강력한 옵션이지만, 최대한 활용하려면 CSS 동작 방식을 이해하고 있어야 한다. 위에서 설명한 디버그 파이프라인 옵션을 사용하면 입력 문서에 어떤 CSS가 들어 있는지 확인할 수 있다.

더 간단한 방법으로 :guilabel:`스타일 정보 필터`를 사용할 수 있다. 이 기능은 지정한 유형의 CSS 속성을 문서에서 모두 제거한다. 예를 들어 모든 색상이나 모든 글꼴을 제거하는 데 사용할 수 있다.

스타일 변형

이 기능은 스타일 관련 기능 중 가장 강력하다. 여러 조건에 따라 스타일을 바꾸는 규칙을 정의할 수 있다. 예를 들어 모든 녹색을 파란색으로 바꾸거나, 텍스트의 굵게 스타일을 모두 제거하거나, 모든 제목을 특정 색으로 칠하는 등의 작업을 할 수 있다.

HTML 변환

스타일 변환과 비슷하지만, 책의 HTML 내용 자체를 바꿀 수 있다. 태그를 다른 태그로 바꾸거나, 내용에 따라 태그에 클래스나 다른 속성을 추가하는 등의 작업이 가능하다.

페이지 설정

페이지 설정 옵션은 여백과 화면 크기처럼 화면 배치를 제어하기 위한 것이다. 선택한 출력 형식이 페이지 여백을 지원하면, 출력 플러그인이 사용할 페이지 여백을 설정할 수 있다. 또한 입력 프로필과 출력 프로필도 선택해야 한다. 이 두 프로필은 기본적으로 입력/출력 문서의 치수 해석 방식, 화면 크기, 기본 글꼴 재조정 키를 다룬다.

변환하려는 파일이 특정 장치/소프트웨어 플랫폼용으로 만들어졌다는 것을 알고 있다면 그에 맞는 입력 프로필을 선택하고, 아니면 기본 입력 프로필을 선택하면 된다. 만들어낼 파일이 특정 장치 유형용이라는 것을 알고 있다면 해당 출력 프로필을 선택하라. 그렇지 않다면 일반(Generic) 출력 프로필 중 하나를 선택하면 된다. MOBI나 AZW3로 변환하는 경우에는 거의 항상 Kindle 출력 프로필 중 하나를 선택하는 것이 좋다. 그 외의 최신 전자책 리더 장치라면 Generic e-ink HD 출력 프로필이 가장 무난하다.

출력 프로필은 화면 크기도 제어한다. 예를 들어 일부 출력 형식에서는 이미지가 화면에 맞도록 자동으로 크기 조정된다. 따라서 자신의 장치와 비슷한 화면 크기를 가진 장치의 프로필을 선택해야 한다.

합리적 추측에 의한(Heuristic) 처리

휴리스틱 처리는 서식이 좋지 않은 입력 문서에서 흔히 발생하는 문제를 감지하고 수정하기 위해 사용할 수 있는 여러 기능을 제공한다. 입력 문서 제작자가 서식 처리를 충분히 하지 않은 경우에 이 기능을 사용하라. 이런 옵션들은 때때로 문서를 훼손할 수도 있으므로 주의해서 써야 한다. 예를 들어 여기 있는 몇몇 옵션은 줄바꿈 불가 공백(non-breaking-space) 엔티티를 모두 제거하거나, 기능과 관련된 오탐지를 포함할 수 있다.

휴리스틱 처리 활성화

이 옵션은 변환 파이프라인에서 calibre의 휴리스틱 처리 단계를 활성화한다. 여러 하위 기능이 적용되려면 이 옵션이 반드시 켜져 있어야 한다.

줄 펼치기

이 옵션을 켜면 calibre가 문서 안에 있는 강제 줄바꿈을 문장부호 단서와 줄 길이를 이용해 감지하고 수정하려고 시도한다. calibre는 먼저 강제 줄바꿈이 실제로 존재하는지 확인하며, 없어 보이면 줄 펼치기를 시도하지 않는다. 줄 펼치기 계수를 낮추면 calibre가 더 적극적으로 줄을 펼치도록 “강제”할 수 있다.

줄 펼치기 계수

이 옵션은 calibre가 강제 줄바꿈을 제거할 때 사용하는 알고리즘을 제어한다. 예를 들어 이 값이 0.4이면, 문서의 모든 줄 길이 중 40%보다 짧은 길이의 줄 끝에 있는 강제 줄바꿈을 calibre가 제거한다는 뜻이다. 수정이 필요한 줄바꿈이 몇 개만 있다면, 이 값은 0.1~0.2 정도로 낮추는 것이 좋다.

서식 없는 장 제목과 부제목 감지 및 마크업

문서에서 장 제목과 타이틀이 본문과 다른 서식으로 표시되어 있지 않다면, calibre는 이 옵션을 사용해 그것들을 감지하고 제목 태그로 감쌀 수 있다. 장 제목에는 <h2> 태그를, 감지된 타이틀에는 <h3> 태그를 사용한다.

이 기능 자체가 TOC를 만들지는 않지만, 많은 경우 calibre의 기본 장 감지 설정이 장을 올바르게 감지하여 TOC를 만들도록 도와준다. TOC가 자동으로 생성되지 않으면 구조 감지의 XPath를 조정하라. 문서에 다른 제목이 없다면 구조 감지에서 “//h:h2”를 설정하는 것이 TOC를 만드는 가장 쉬운 방법이다.

삽입된 제목은 서식이 적용되지 않으므로, 서식을 주려면 모양새 변환 설정 아래의 추가 CSS 옵션을 사용해야 한다. 예를 들어 제목 태그를 가운데 정렬하려면 다음을 사용하라:

h2, h3 { text-align: center }

<h1> 또는 <h2> 태그 연속 번호 다시 매기기

일부 출판사는 장 제목을 여러 개의 <h1> 또는 <h2> 태그를 연속해서 사용해 서식 지정한다. calibre의 기본 변환 설정을 사용하면 이런 제목은 두 조각으로 나뉠 수 있다. 이 옵션은 제목 태그의 번호를 다시 매겨 분할을 방지한다.

문단 사이 빈 줄 삭제

이 옵션을 켜면 calibre가 문서 안에 들어 있는 빈 줄을 분석한다. 모든 문단 사이에 빈 줄이 하나씩 끼어 있다면, calibre는 그 빈 문단을 모두 제거한다. 여러 개의 빈 줄이 연속된 경우에는 장면 전환(scene break)으로 간주하여 하나의 문단으로 유지한다. 이 옵션은 모양새 아래의 문단 간격 제거`와 달리 HTML 내용 자체를 수정하고, 다른 옵션은 문서의 스타일만 수정한다. 또한 이 옵션은 calibre의 :guilabel:`빈 줄 삽입 옵션으로 삽입된 문단도 제거할 수 있다.

장면 전환이 일관된 서식이 되도록 보장

이 옵션을 사용하면 calibre가 흔한 장면 전환 표시를 감지하여 가운데 정렬되도록 맞추려고 시도한다. ‘부드러운’ 장면 전환 표시, 즉 추가 공백만으로 정의된 장면 전환은 페이지 나눔과 함께 표시되지 않도록 스타일이 조정된다.

장면 나누기 교체

이 옵션을 설정하면 calibre는 발견한 장면 전환 표시를 사용자가 지정한 대체 텍스트로 바꾼다. 다만 장식용 문자는 일부 리더 장치에서 지원되지 않을 수 있다는 점에 유의하라.

일반적으로 HTML 태그는 사용하지 않는 것이 좋다. calibre는 태그를 버리고 미리 정의된 마크업을 사용한다. 예외는 <hr /> 태그, 즉 가로줄과 <img> 태그이다. 가로줄에는 선택적으로 스타일을 지정할 수 있는데, 직접 스타일을 추가한다면 반드시 ‘width’ 설정을 포함해야 하며 그렇지 않으면 스타일 정보가 버려진다. 이미지 태그도 사용할 수 있지만, calibre는 변환 중 이미지를 추가하는 기능을 제공하지 않으므로, 나중에 ‘책 편집’ 기능으로 추가해야 한다.

이미지 태그 예시(변환 후 EPUB 내부의 ‘Images’ 폴더 안에 이미지를 넣어 두어야 함):

<img style=”width:10%” src=”../Images/scenebreak.png” />

스타일이 적용된 가로줄 예시:

<hr style=”width:20%;padding-top: 1px;border-top: 2px ridge black;border-bottom: 2px groove black;”/>

불필요한 하이픈 제거하기

이 옵션을 켜면 calibre는 문서의 모든 하이픈 연결 내용을 분석한다. 분석에는 문서 자체를 사전처럼 사용한다. 덕분에 어떤 언어의 단어든, 심지어 조어와 생소한 과학 용어까지도 정확하게 하이픈을 제거할 수 있다. 주요 단점은 문서에 한 번만 나오는 단어는 바뀌지 않는다는 점이다. 분석은 두 번에 걸쳐 이루어진다. 첫 번째 단계에서는 줄 끝을 분석하며, 문서 안에 하이픈이 있거나 없는 형태가 둘 다 존재하는 경우에만 줄을 펼친다. 두 번째 단계에서는 문서 전체의 하이픈 단어를 분석하며, 같은 단어가 문서 다른 곳에 하이픈 없이 존재하면 하이픈을 제거한다.

일반적인 단어와 패턴을 이탤릭체로 표시

활성화하면 calibre는 이탤릭체를 의미하는 일반적인 단어와 패턴을 찾아 이탤릭체로 바꾼다. 예를 들어 ~word~ 같은 흔한 텍스트 관례나, 일반적으로 이탤릭체로 써야 하는 표현(예: ‘etc.’ 또는 ‘et cetera’ 같은 라틴어 구절)을 처리한다.

엔티티 들여쓰기를 CSS 들여쓰기로 바꾸기

일부 문서는 줄바꿈 불가 공백 엔티티를 사용해 텍스트 들여쓰기를 표현하는 관례를 사용한다. 이 옵션을 켜면 calibre가 이런 서식을 감지해 CSS의 3% 텍스트 들여쓰기로 변환하려고 시도한다.

검색 & 교체

이 옵션들은 주로 PDF 문서나 OCR 변환물을 변환할 때 유용하지만, 문서마다 존재하는 여러 문제를 고치는 데도 사용할 수 있다. 예를 들어 어떤 변환물은 페이지 머리글과 바닥글이 본문 안에 남는다. 이 옵션들은 정규식을 사용해 머리글, 바닥글 또는 다른 임의의 텍스트를 감지하고 제거하거나 바꾸려고 시도한다. 이 기능은 변환 파이프라인이 생성한 중간 XHTML에 작동한다는 점을 기억하라. 문서에 맞는 정규식을 만드는 데 도움이 되는 마법사도 있다. 식 상자 옆의 마법봉을 눌러 검색식을 작성한 뒤 ‘시험’ 버튼을 누르면 된다. 일치에 성공한 부분은 노란색으로 강조된다.

검색은 Python 정규식을 사용해 수행된다. 일치한 텍스트는 문서에서 단순히 제거되거나 대체 패턴을 사용해 바뀐다. 대체 패턴은 선택 사항이며, 비워 두면 검색 패턴과 일치한 텍스트는 문서에서 삭제된다. 정규식과 그 문법에 대해서는 :ref:`regexptutorial`에서 더 자세히 배울 수 있다.

구조 탐지

구조 감지는 입력 문서에 구조 요소가 제대로 지정되어 있지 않을 때, calibre가 장, 페이지 나눔, 머리글, 바닥글 같은 요소를 최대한 감지하려고 시도하는 과정이다. 짐작할 수 있듯이 이 과정은 책마다 크게 다르다. 다행히 calibre에는 이를 제어할 수 있는 매우 강력한 옵션이 있다. 강력한 만큼 복잡하지만, 그 복잡함을 익히는 데 시간을 들이면 충분히 그만한 가치가 있다.

장과 페이지 나눔
calibre에는 :guilabel:`장 감지`와 :guilabel:`페이지 나눔 삽입`을 위한 두 세트의 옵션이 있다. 기본적으로 calibre는 감지된 장 앞에도 페이지 나눔을 넣고, 페이지 나눔 옵션으로 감지된 위치에도 페이지 나눔을 넣기 때문에 약간 헷갈릴 수 있다. 그 이유는 페이지 나눔이 필요하지만 장 경계는 아닌 위치가 자주 있기 때문이다. 또한 감지된 장은 자동 생성된 목차에 선택적으로 삽입할 수도 있다.

calibre는 장 경계/페이지 나눔을 지정할 수 있도록 XPath 라는 강력한 언어를 사용한다. XPath는 처음에는 다소 어렵게 느껴질 수 있지만, 다행히 사용자 설명서에 :ref:`XPath tutorial <xpath-tutorial>`이 있다. 구조 감지는 변환 파이프라인이 만들어 낸 중간 XHTML에 대해 동작한다는 점을 기억하라. 자신의 책에 맞는 설정을 찾으려면 :ref:`conversion-introduction`에서 설명한 디버그 옵션을 사용하라. 간단한 XPath 식을 만드는 데 도움을 주는 XPath 마법사 버튼도 있다.

기본적으로 calibre는 장 감지에 다음 식을 사용한다:

//*[((name()='h1' or name()='h2') and re:test(., 'chapter|book|section|part\s+', 'i')) or @class = 'chapter']

이 식은 여러 흔한 경우를 동시에 처리하려고 하기 때문에 꽤 복잡하다. 의미는 calibre가 (chapter, book, section 또는 part)`라는 단어 중 하나를 포함하거나 `class=”chapter” 속성을 가진 <h1> 또는 <h2> 태그에서 장이 시작된다고 가정한다는 것이다.

관련 옵션으로 :guilabel:`장 표시`가 있다. 이 옵션은 calibre가 장을 감지했을 때 무엇을 할지 제어한다. 기본값은 장 앞에 페이지 나눔을 삽입하는 것이다. 페이지 나눔 대신, 또는 페이지 나눔과 함께 구분선을 넣게 할 수도 있고, 아무것도 하지 않게 할 수도 있다.

페이지 나눔 감지의 기본 설정은 다음과 같다:

//*[name()='h1' or name()='h2']

즉 기본적으로 calibre는 모든 <h1> 및 <h2> 태그 앞에 페이지 나눔을 삽입한다는 뜻이다.

참고

기본 식은 변환하는 입력 형식에 따라 달라질 수 있다.

기타

이 섹션에는 몇 가지 옵션이 더 있다.

책 시작 부분에 메타데이터를 페이지로 삽입

calibre의 큰 장점 중 하나는 평점, 태그, 주석 등 책에 대한 매우 완전한 메타데이터를 관리할 수 있다는 것이다. 이 옵션은 이러한 메타데이터를 모두 담은 단일 페이지를 만들어 변환된 전자책에 삽입한다. 보통은 표지 바로 뒤에 들어간다. 자신만의 맞춤 책 재킷을 만드는 방법이라고 생각하면 된다.

첫 번째 이미지 제거

변환하려는 원본 문서에 표지가 별도의 표지가 아니라 책 본문의 일부로 포함되어 있는 경우가 있다. 이때 calibre에서 따로 표지를 지정하면 변환된 책에는 표지가 두 번 들어갈 수 있다. 이 옵션을 켜면 첫 번째 이미지를 제거해 이런 문제를 막을 수 있다.

목차

입력 문서의 메타데이터에 목차(Table of Contents)가 있으면 calibre는 그것을 그대로 사용한다. 하지만 오래된 형식 중에는 메타데이터 기반 목차를 지원하지 않거나, 지원하더라도 항목이 충분하지 않은 경우가 있다. 이런 경우 calibre가 자동 생성한 목차를 사용해야 한다.

참고

이 옵션들을 정확히 맞추는 일은 다소 까다로울 수 있다. 목차를 직접 만들거나 수정하는 쪽이 더 편하다면 EPUB 또는 AZW3 형식으로 변환한 뒤, 책 편집 기능에서 수동으로 목차를 편집하면 된다.

첫 번째 옵션은 :guilabel:`자동 생성 목차 강제 사용`이다. 이 옵션을 체크하면 calibre가 입력 문서의 메타데이터에서 찾은 목차를 무시하고 자동 생성된 목차를 사용하게 할 수 있다.

자동 생성 목차의 기본 동작 방식은 calibre가 먼저 감지한 장을 생성된 목차에 추가하는 것이다. 그런 다음 생성된 목차에 너무 적은 항목만 있으면 다른 항목을 찾아 추가한다.

감지된 장 수가 :guilabel:`장 임계값`보다 적으면, calibre는 입력 문서에서 발견한 모든 하이퍼링크를 목차에 추가한다. 입력 문서 전체에 걸친 링크는 대개 많기 때문에, 이 기능을 켜면 지나치게 긴 목차가 생길 수 있다는 점에 유의하라.

calibre는 생성된 목차에서 중복 항목을 자동으로 걸러낸다. 하지만 여전히 원하지 않는 항목이 있다면 목차 필터 옵션을 사용해 걸러낼 수 있다.

Next|Previous

수준 1,2,3 TOC 옵션을 사용하면 정교한 다단계 목차를 만들 수 있다. 이것들은 변환 과정에서 생성된 중간 XHTML의 태그와 일치하는 XPath 식이다. 해당 태그에 매핑된 텍스트가 목차 항목이 된다.

예를 들어, 입력 문서가 변환된 뒤 다음과 같은 XHTML이 나온다고 하자:

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Sample document</title>
</head>
<body>
<h1>Chapter 1</h1>
...
<h2>Section 1.1</h2>
...
<h2>Section 1.2</h2>
...
<h1>Chapter 2</h1>
...
<h2>Section 2.1</h2>
...
</body>
</html>

그러면 옵션을 다음처럼 설정한다:

Level 1 TOC : //h:h1
Level 2 TOC : //h:h2

이렇게 하면 자동 생성된 2단계 목차가 다음과 같이 만들어진다:

Chapter 1
Section 1.1
Section 1.2
Chapter 2
Section 2.1

경고

모든 출력 형식이 다단계 목차를 지원하는 것은 아니다. 먼저 EPUB 출력으로 시험해 보고, 잘 되면 원하는 형식에서도 시도하라.

HTML 입력 문서를 변환할 때 이미지를 장 제목으로 사용하기

장 제목으로 이미지를 사용하고 싶지만, 동시에 calibre가 장 제목을 바탕으로 자동으로 목차를 생성할 수 있게 하려면 다음과 같이 하라:

<html>
<body>
<h2>Chapter 1</h2>
<p>chapter 1 text...</p>
<h2 title="Chapter 2"><img src="chapter2.jpg" /></h2>
<p>chapter 2 text...</p>
</body>
</html>

수준 1 TOC 설정을 //h:h2``로 지정하라. 그러면 두 번째 장의 경우 calibre는 ``<h2> 태그의 title 속성 값을 제목으로 사용한다. 태그 자체의 텍스트가 비어 있기 때문이다.

목차 항목의 텍스트를 태그 속성으로 제공하기

장 제목이 너무 길어서 목차에는 더 짧은 버전을 넣고 싶다면 title 속성을 사용하면 된다. 예를 들면 다음과 같다:

<html>
<body>
<h2 title="Chapter 1">Chapter 1: Some very long title</h2>
<p>chapter 1 text...</p>
<h2 title="Chapter 2">Chapter 2: Some other very long title</h2>
<p>chapter 2 text...</p>
</body>
</html>

수준 1 TOC 설정을 //h:h2/@title``로 지정하라. 그러면 calibre는 ``<h2> 태그의 텍스트를 쓰는 대신 title 속성 값을 제목으로 사용한다.

변환 옵션의 설정/저장 방식
calibre에서 변환 옵션을 설정할 수 있는 곳은 두 군데다. 첫 번째는 환경설정->변환이다. 여기의 설정은 변환 옵션의 기본값이다. 새 책을 변환할 때마다 이 설정이 사용된다.

각 책 변환마다 변환 대화상자에서 설정을 바꿀 수도 있다. calibre는 책을 변환할 때 그 책에 사용한 설정을 기억하므로, 나중에 같은 책을 다시 변환하면 그 저장된 책별 설정이 사용된다.

여러 권의 책을 일괄 변환할 때는 설정이 다음 순서로 적용된다(나중 것이 우선한다):

- 환경설정->변환에서 설정한 기본값

- 변환 중인 각 책에 저장된 변환 설정(있는 경우). 이 동작은 일괄 변환 대화상자 왼쪽 위의 옵션으로 끌 수 있다.

- 일괄 변환 대화상자에서 지정한 설정

일괄 변환에서 각 책에 최종 적용된 설정은 저장되며, 그 책을 다시 변환할 때 재사용된다는 점에 유의하라. 일괄 변환에서는 일괄 변환 대화상자의 설정 우선순위가 가장 높기 때문에, 일괄 변환을 수행하면 기존에 저장되어 있던 책별 설정을 덮어쓰게 되는 경우가 많다.

참고

오른쪽 아래의 회전하는 아이콘을 클릭한 다음 개별 변환 작업을 두 번 클릭하면, 어떤 변환에서 실제로 사용된 설정을 확인할 수 있다. 그러면 로그와 함께 변환에 사용된 정확한 옵션이 표시된다.

형식별 팁

여기서는 특정 형식을 변환할 때 유용한 팁을 소개한다. 특정 형식에만 해당하는 옵션은 입력이든 출력이든 변환 대화상자의 해당 섹션에서 사용할 수 있다.

Microsoft Word 문서 변환
calibre는 Microsoft Word 2007 이상에서 만든 .docx 파일을 자동으로 변환할 수 있다. 파일을 calibre에 추가한 뒤 변환을 클릭하면 된다.

참고
calibre 변환 엔진의 기능을 보여 주는 :download_file:`데모 .docx 파일 <demos/demo.docx>`도 있다. 내려받아 EPUB이나 AZW3로 변환해 보면 된다.

Microsoft Word에서 제목에 제목 1, 제목 2 등의 스타일을 지정해 두면 calibre는 이를 바탕으로 목차를 자동 생성한다. 출력 결과를 더 잘 제어하려면 Word에서 스타일과 문서 구조를 올바르게 사용하는 것이 중요하다.

오래된 .doc 파일

오래된 .doc 파일의 경우 Microsoft Word에서 문서를 HTML로 저장한 뒤, 생성된 HTML 파일을 calibre로 변환할 수 있다. HTML로 저장할 때는 반드시 “웹 페이지, 필터링됨” 저장 옵션을 사용하라. 그러면 불필요한 Word 전용 마크업이 많이 줄어든다.

또 다른 방법으로 무료 LibreOffice를 사용할 수도 있다. LibreOffice에서 .doc 파일을 열어 .docx로 저장하면 calibre에서 바로 변환할 수 있다.

TXT 문서 변환

TXT 문서에는 굵게, 이탤릭체 같은 서식이나 문단, 제목, 절 같은 문서 구조를 명확히 지정하는 표준적인 방법이 없다. 하지만 실제로는 여러 가지 규칙과 관례가 사용되며, calibre는 이를 처리할 수 있는 다양한 옵션을 제공한다.

TXT 입력은 문단을 어떻게 감지할지 구분하기 위한 여러 옵션을 지원한다.

문단 스타일: 자동

텍스트 파일을 분석하여 문단이 어떻게 정의되어 있는지 자동으로 판단하려고 시도한다. 일반적으로는 잘 작동하지만 원하는 결과가 나오지 않으면 아래의 다른 문단 스타일 중 하나를 시도해 보라.

문단 스타일: 블록

하나 이상의 빈 줄을 문단 경계로 가정한다:

This is the first.

This is the
second paragraph.

문단 스타일: 단일

모든 줄을 하나의 문단으로 가정한다:

This is the first.
This is the second.
This is the third.

문단 스타일: 인쇄본

모든 문단이 들여쓰기(탭 또는 공백 2칸 이상)로 시작한다고 가정한다. 다음 들여쓰기 줄이 나오면 이전 문단이 끝난 것으로 본다:

This is the
first.
This is the second.

This is the
third.

문단 스타일: 무서식

문서에 서식은 없지만 강제 줄바꿈은 사용한다고 가정한다. 문장부호와 평균 줄 길이를 이용해 문단을 다시 만들어 내려고 시도한다.

서식 스타일: 자동

사용 중인 서식 마크업 유형을 감지하려고 시도한다. 아무 마크업도 사용되지 않았다면 휴리스틱 서식이 적용된다.

서식 스타일: 휴리스틱

문서에서 흔한 장 제목, 장면 전환, 이탤릭체 단어를 분석하여 변환 중 적절한 HTML 마크업을 적용한다.

서식 스타일: Markdown

calibre는 Markdown이라는 변환 전처리기를 거쳐 TXT 입력을 처리하는 것도 지원한다. Markdown을 사용하면 굵게, 이탤릭체, 제목, 목록 등과 같은 기본 서식을 TXT 문서에 추가할 수 있다.

서식 스타일: 없음

텍스트에 특별한 서식을 적용하지 않는다. 문서는 다른 변경 없이 HTML로 변환된다.

PDF 문서 변환

PDF 문서는 변환 원본으로는 최악의 형식 중 하나다. PDF는 고정 페이지 크기와 고정 텍스트 배치 형식이기 때문이다. 즉, 어느 지점에서 한 문단이 끝나고 다음 문단이 시작되는지 판단하기가 매우 어렵다.

또한 머리글과 바닥글이 문서 일부로 포함되어 있는 경우가 많아서 본문과 함께 들어가 버리기도 한다. 머리글과 바닥글을 제거하려면 검색 및 바꾸기 패널을 사용하라.

PDF 입력의 몇 가지 제한 사항은 다음과 같다:

- 복잡한 다단 문서와 이미지 기반 문서는 지원되지 않는다.

- 문서 안에 있는 벡터 이미지와 표의 추출도 지원되지 않는다.

- 일부 PDF는 ll, ff, fi 등을 표현하기 위해 특수 글리프를 사용한다. 이런 경우의 변환이 제대로 될지는 PDF 내부에서 그것들이 어떻게 표현되어 있는지에 달려 있다.

- 링크와 목차는 지원되지 않는다.

- 비영어 문자를 표현하기 위해 내장된 비유니코드 글꼴을 사용하는 PDF는 해당 문자가 깨진 출력으로 변환된다.

- 일부 PDF는 페이지 사진 위에 OCR 텍스트를 얹어 만든다. 이런 경우 calibre는 OCR된 텍스트를 사용하며, 이는 화면에서 보이는 내용과 상당히 다를 수 있다.

- 오른쪽에서 왼쪽으로 쓰는 언어 또는 수식 조판처럼 복잡한 텍스트를 표시하는 데 사용되는 PDF는 올바르게 변환되지 않는다.

다시 한 번 강조하지만 PDF는 입력 형식으로 정말, 정말 좋지 않다. 꼭 PDF를 사용해야 한다면, 원본 PDF의 구조에 따라 괜찮은 수준에서 거의 사용할 수 없는 수준까지 결과가 크게 달라질 수 있다는 점을 각오해야 한다.

만화책 묶음

만화책 묶음은 .cbc 파일이다. .cbc 파일은 다른 CBZ/CBR 파일을 담고 있는 ZIP 파일이다. 또한 .cbc 파일에는 각 만화책 제목을 한 줄에 하나씩 적은 comics.txt라는 간단한 텍스트 파일이 반드시 들어 있어야 한다.

one.cbz:Chapter One
two.cbz:Chapter Two
three.cbz:Chapter Three

그러면 .cbc 파일에는 다음이 들어가게 된다:

comics.txt
one.cbz
two.cbz
three.cbz

calibre는 이 .cbc 파일을 자동으로 각 comics.txt 항목을 가리키는 목차가 포함된 전자책으로 변환한다.

EPUB 고급 서식 데모

다양한 EPUB 고급 서식은 이 :download_file:`데모 파일 <demos/demo.epub>`에서 확인할 수 있다. 이 파일은 calibre를 이용해 손으로 작성한 HTML에서 만들어졌으며, EPUB이 지원할 수 있는 여러 고급 기능을 보여 주기 위한 것이다.

이 파일을 만든 원본 HTML은 :download_file:`demo.zip <demos/demo.zip>`에서 받을 수 있다. ZIP 파일에서 EPUB을 만들 때 사용한 설정은 다음과 같다:

ebook-convert demo.zip .epub -vv --authors "Kovid Goyal" --language en --level1-toc '//*[@class="title"]' --disable-font-rescaling --page-breaks-before / --no-default-epub-cover

이 파일은 EPUB의 잠재력을 보여 주기 위한 것이므로, 대부분의 고급 서식은 calibre 내장 EPUB 뷰어보다 기능이 떨어지는 리더에서는 작동하지 않을 수 있다는 점에 유의하라.

ODT 문서 변환
calibre는 ODT(OpenDocument Text) 파일을 직접 변환할 수 있다. 문서 서식에는 스타일을 사용하고 직접 서식 지정의 사용은 최소화하는 것이 좋다. 문서에 이미지를 삽입할 때도 배치를 단순하게 유지하는 편이 좋다.

장을 자동으로 감지하려면 제목 1, 제목 2, …, 제목 6 (Heading 1 ~ Heading 6)이라 불리는 기본 제공 스타일로 장을 표시해야 한다.

- 장을 제목 2 스타일로 표시했다면 ‘장에서 감지’ 상자에 ``//h:h2``를 설정해야 한다.

- 제목 2`로 표시한 절과 :guilabel:`제목 3`으로 표시한 장이 있는 중첩 TOC를 만들려면 `//h:h2|//h:h3``를 입력해야 한다. 변환 - TOC 페이지에서는 해당 수준 1/수준 2 설정에 맞게 값을 넣어라.

잘 알려진 문서 속성(제목, 키워드, 설명, 작성자)은 인식되며, calibre는 첫 번째 이미지(너무 작지 않고 가로세로 비율이 적당한 것)를 표지로 사용한다.

고급 속성 변환 모드도 있다. ODT 문서의 사용자 정의 속성(File->Properties)에 opf.metadata 사용자 속성(‘예/아니오’ 형식)을 Yes로 설정하면 활성화된다.

opf.titlesort
opf.authors
opf.authorsort
opf.publisher
opf.pubdate
opf.isbn
opf.language
opf.series
opf.seriesindex

이와 함께 ODT 안에서 표지로 사용할 그림의 이름을 ``opf.cover``로 지정할 수도 있다(오른쪽 클릭, 그림->옵션->이름). 이 이름의 그림을 찾지 못하면 일반 표지 감지 방식이 사용된다.

고급 모드에서 표지 감지를 끄려면 opf.nocover 사용자 속성(‘예/아니오’ 형식)을 Yes로 설정하면 된다.

PDF로 변환하기

PDF로 변환할 때 가장 먼저, 그리고 가장 중요하게 결정해야 할 설정은 페이지 크기다. 기본적으로 calibre는 “U.S. Letter” 페이지 크기를 사용한다. 필요에 따라 A4 같은 다른 표준 크기로 바꿀 수 있다. 독자의 용도와 인쇄 환경에 맞는 크기를 선택하라.

머리글과 바닥글

머리글/바닥글 템플릿을 지정하면 PDF의 각 페이지에 임의의 머리글과 바닥글을 삽입할 수 있다. 템플릿은 머리글/바닥글 영역에서 렌더링되는 간단한 HTML 코드 조각일 뿐이다.

<footer><div style="margin: auto; color: green">_PAGENUM_</div></footer>

calibre는 :code:`_PAGENUM_`을 현재 페이지 번호로 자동 치환한다. 짝수 페이지와 홀수 페이지에 서로 다른 내용을 넣을 수도 있다. 예를 들어 다음 머리글 템플릿과 같이 사용할 수 있다:

<header style="justify-content: flex-end">
<div class="even-page">_AUTHOR_</div>
<div class="odd-page"><i>_TITLE_</i></div>
</header>

calibre는 :code:`_TITLE_`과 :code:`_AUTHOR_`를 변환 중인 문서의 제목과 저자로 자동 치환한다. :code:`justify-content`를 :code:`flex-end`로 설정하면 내용을 오른쪽 정렬할 수 있다.

다음 머리글 템플릿 예시처럼, 왼쪽과 오른쪽 가장자리에 서로 다른 텍스트를 표시하고 글꼴 크기를 바꿀 수도 있다:

<header style="justify-content: space-between; font-size: smaller">
<div>_TITLE_</div>
<div>_AUTHOR_</div>
</header>

이렇게 하면 본문보다 작은 글꼴 크기로, 왼쪽에는 제목이, 오른쪽에는 저자가 표시된다.

아래와 같이 템플릿에서 현재 절(section)도 사용할 수 있다:

<header><div>_SECTION_</div></header>

:code:`_SECTION_`은 현재 절의 이름으로 치환된다. 이 이름은 문서의 메타데이터 목차(PDF 개요)에서 가져온다. 문서에 개요가 없으면 비어 있게 된다.

머리글과 바닥글 템플릿 안에서 JavaScript를 사용할 수도 있다. 예를 들어 다음 템플릿은 페이지 번호가 1이 아니라 4에서 시작하도록 만든다:

<footer>
<div></div>
<script>document.currentScript.parentNode.querySelector("div").innerHTML = "" + (_PAGENUM_ + 3)</script>
</footer>

또한 머리글과 바닥글에서 사용할 수 있는 변수가 몇 가지 더 있으며, 아래에 설명되어 있다:

- _TOTAL_PAGES_ - PDF 파일의 전체 페이지 수. 예를 들어 진행 상태 카운터를 구현할 때 유용하다.

- _TOP_LEVEL_SECTION_PAGES_ - 현재 최상위 절의 전체 페이지 수

- _TOP_LEVEL_SECTION_PAGENUM_ - 현재 최상위 절 안에서 현재 페이지가 몇 번째 페이지인지

- _WIDTH_PIXELS_ - 머리글/바닥글 영역의 픽셀 단위 너비

- _HEIGHT_PIXELS_ - 머리글/바닥글 영역의 픽셀 단위 높이

참고

머리글과 바닥글을 추가할 때는 변환 대화상자의 PDF 출력 섹션에서 페이지 위쪽 및 아래쪽 여백을 충분히 크게 설정해야 한다.

인쇄용 목차

각 절의 페이지 번호를 나열하는 인쇄용 목차를 PDF 끝에 삽입할 수도 있다. PDF를 실제 종이에 인쇄하려는 경우 특히 유용하다.

생성된 목차의 모양은 변환 대화상자의 모양새 부분에 있는 추가 CSS 설정을 사용해 사용자화할 수 있다. 기본으로 사용되는 CSS는 다음과 같다:

.calibre-pdf-toc table { width: 100%% }

.calibre-pdf-toc table tr td:last-of-type { text-align: right }

.calibre-pdf-toc .level-0 {
font-size: larger;
}

.calibre-pdf-toc .level-1 td:first-of-type { padding-left: 1.4em }
.calibre-pdf-toc .level-2 td:first-of-type { padding-left: 2.8em }

개별 HTML 파일에 대한 사용자 지정 페이지 여백

여러 개의 개별 HTML 파일이 들어 있는 EPUB 또는 AZW3 파일을 변환할 때 특정 HTML 파일의 페이지 여백만 바꾸고 싶다면, 그 HTML 파일의 <head> 섹션에 다음 CSS를 추가할 수 있다:

<style>
@page {
margin-left: 10pt;
margin-right: 10pt;
margin-top: 10pt;
margin-bottom: 10pt;
}
</style>

그런 다음 변환 대화상자의 PDF 출력 섹션에서 변환 중인 문서의 페이지 여백 사용 옵션을 켜라. 그러면 이 HTML 파일에서 생성된 모든 페이지에는 지정한 사용자 지정 여백이 적용되고, 나머지 페이지는 PDF 출력 설정에서 지정한 여백을 사용한다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 전자책 편집하는 중

전자책 편집하는 중
calibre에는 EPUB, KEPUB(Kobo), AZW3(Kindle) 형식의 책을 편집할 수 있는 통합 전자책 편집기가 포함되어 있습니다. 이 편집기는 책 파일 내부에서 사용되는 HTML과 CSS를 보여 주며, 변경 사항을 적용하면 실시간으로 업데이트되는 미리보기를 함께 제공합니다. 또한 일반적인 정리 및 수정 작업을 수행하는 다양한 자동화 도구도 포함하고 있습니다.

calibre에서 아무 책이나 마우스 오른쪽 버튼으로 클릭한 다음 :guilabel:`책 편집`을 선택하면 이 편집기를 사용할 수 있습니다.

목차

- 기본 작업흐름

- 파일 탐색기

- 파일 이름 바꾸기

- 파일 병합

- 텍스트 파일 순서 변경

- 표지 지정

- 파일 삭제

- 파일 내보내기

- 새 이미지/글꼴/기타 추가 또는 새 빈 파일 만들기

- 파일 바꾸기

- HTML 파일에 스타일시트 효율적으로 연결하기

- 검색 & 교체

- 저장된 검색

- 함수 모드

- HTML 태그를 무시하고 검색

- 자동화 도구

- 목차 편집

- 책 검사

- 표지 추가

- 참조된 글꼴 포함하기

- 내장된 글꼴의 사용된 문자만 취하기 (Subsetting embedded fonts)

- 지능적 문장부호 처리

- CSS 속성 변형하는 중

- 사용하지 않는 CSS 규칙 제거 중

- HTML 수정

- 파일 서식 정리

- 본문 안에 목차 삽입

- 의미 정보 설정

- 스타일 정보 필터링

- 책 내부 구조 업그레이드

- 체크포인트

- 실시간 미리보기 패널

- HTML 파일 분할

- 실시간 CSS 패널

- 기타 도구

- 목차 보기

- 책에 있는 단어의 철자 검사

- 특수 문자 삽입

- 코드 검사기 보기

- 외부 링크 검사

- 외부 리소스 다운로드 중

- 파일을 형식별 폴더로 정리

- 다른 전자책 형식의 파일을 EPUB로 가져오는 중

- 보고서 도구

- 코드 편집기의 특수 기능

- 구문 강조표시

- 문맥에 맞는 도움말

- 자동 완성

- 토막글

기본 작업흐름

참고
calibre 전자책 편집기의 동영상 안내는 :website:`여기 <demo#tutorials>`에서 볼 수 있습니다.

책 편집 도구로 책을 처음 열면 왼쪽에 파일 목록이 표시됩니다. 여기에는 책의 내용을 구성하는 개별 HTML 파일, 스타일시트, 이미지 등이 포함됩니다. 파일을 두 번 클릭하면 바로 편집을 시작할 수 있습니다. 다만 몇 가지 작은 수정 이상의 작업을 하려면 `HTML Tutorial <https://www.w3schools.com/Html/default.asp>`_과 `CSS Tutorial <https://www.w3schools.com/css/default.asp>`_을 알고 있어야 합니다.

편집기에서 HTML이나 CSS를 수정하면 오른쪽 미리보기 패널에 변경 사항이 실시간으로 반영됩니다. 변경 결과가 마음에 들면 저장 버튼을 클릭하거나 :guilabel:`파일->저장`을 사용해 변경 내용을 전자책에 저장하십시오.

유용한 기능 중 하나가 :guilabel:`체크포인트`입니다. 본격적인 편집을 시작하기 전에 체크포인트를 만들 수 있습니다. 체크포인트는 현재 책의 상태를 보존해 두므로, 나중에 변경 결과가 마음에 들지 않으면 체크포인트를 만든 시점의 상태로 되돌릴 수 있습니다. 체크포인트를 만들려면 :guilabel:`편집->체크포인트 만들기`를 사용하십시오. 전역 검색 및 바꾸기 같은 자동화 도구를 실행할 때도 체크포인트가 자동으로 만들어집니다. 체크포인트 기능은 개별 파일 편집 시 사용하는 일반 실행 취소/다시 실행 기능과 별개입니다. 체크포인트는 변경 사항이 책의 여러 파일에 걸쳐 있을 때 특히 필요합니다.

이것이 책 편집의 기본 작업 흐름입니다. 즉, 파일을 열고, 수정하고, 미리 본 뒤 저장하는 것입니다. 이 설명서의 나머지 부분에서는 특정 작업을 효율적으로 수행할 수 있도록 다양한 도구와 기능을 설명합니다.

파일 탐색기

:guilabel:`파일 탐색기`는 편집 중인 책 내의 다양한 파일에 대한 개요를 제공합니다. 파일은 범주별로 정렬되며 상단에는 텍스트(HTML) 파일이 있고 그 다음에는 스타일시트(CSS) 파일, 이미지 등이 있습니다. 파일을 두 번 클릭하면 편집이 시작됩니다. HTML, CSS 및 이미지 파일에 대한 편집이 지원됩니다. 텍스트 파일의 순서는 책을 읽고 있는 경우 표시되는 순서와 같습니다. 다른 모든 파일은 알파벳순으로 정렬됩니다.

항목 위에 마우스를 올리면 파일 크기를 볼 수 있고, 화면 아래쪽에서는 책 내부의 해당 파일 전체 경로도 확인할 수 있습니다. 전자책 내부의 파일은 압축되어 있으므로 최종 책 크기는 개별 파일 크기의 합과 같지 않다는 점에 유의하십시오.

책 안의 많은 파일에는 특별한 의미가 있습니다. 이런 파일은 보통 이름 옆에 아이콘이 표시되어 그 의미를 알려 줍니다. 예를 들어 왼쪽 그림에서는 cover_image.jpg`와 :guilabel:`titlepage.xhtml 파일 이름 옆에 표지 아이콘이 보이는데, 이는 각각 책 표지 이미지와 표지 페이지라는 뜻입니다. 마찬가지로 content.opf 파일 옆의 메타데이터 아이콘은 이 파일에 책 메타데이터가 들어 있음을 뜻하고, toc.ncx 파일 옆의 T 아이콘은 이것이 목차 파일임을 뜻합니다.

개별 파일을 마우스 오른쪽 버튼으로 클릭하면 다양한 작업을 수행할 수 있습니다.

파일 이름 바꾸기

개별 파일은 마우스 오른쪽 버튼으로 클릭한 뒤 :guilabel:`이름 바꾸기`를 선택해 이름을 바꿀 수 있습니다. 파일 이름을 바꾸면 책 전체에서 해당 파일을 가리키는 모든 링크와 참조가 자동으로 업데이트됩니다. 따라서 새 이름만 입력하면 나머지는 calibre가 처리합니다.

여러 파일의 이름을 한 번에 바꿀 수도 있습니다. 파일 이름에 단순한 규칙을 적용하고 싶을 때 유용합니다. 예를 들어 모든 HTML 파일의 이름을 Chapter-1.html, Chapter-2.html처럼 바꾸고 싶을 수 있습니다. Shift 또는 Ctrl 키를 누른 채 파일을 클릭해 일괄 이름 바꾸기를 적용할 파일을 선택하십시오. 그런 다음 마우스 오른쪽 버튼을 눌러 :guilabel:`일괄 이름 바꾸기`를 선택합니다. 접두어와 자동 번호 매기기를 시작할 숫자를 입력한 뒤 확인을 누르면 끝입니다. 일괄 이름 바꾸기 대화 상자에서는 선택한 순서가 아니라 책에 나타나는 순서대로 파일 이름을 바꾸는 기능도 제공하므로, 예를 들어 모든 이미지를 등장 순서대로 이름 바꿀 때 유용합니다.

마지막으로 선택한 모든 파일의 확장자를 한 번에 바꿀 수도 있습니다. 위와 같이 여러 파일을 선택한 다음 마우스 오른쪽 버튼을 눌러 :guilabel:`선택한 파일의 확장자 변경`을 선택하십시오.

파일 병합

때로는 두 HTML 파일이나 두 CSS 파일을 하나로 합치고 싶을 수 있습니다. 모든 내용을 하나의 파일에 넣는 것이 유용할 때가 있습니다. 다만 많은 내용을 하나의 파일에 넣으면 일반적인 전자책 리더에서 책을 볼 때 성능 문제가 생길 수 있으므로 주의하십시오.

여러 파일을 병합하려면 Ctrl 키를 누른 채 파일을 클릭해 선택하십시오. 이때 반드시 한 종류의 파일만 선택해야 합니다. 즉, HTML 파일만 선택하거나 CSS 파일만 선택해야 합니다. 그런 다음 마우스 오른쪽 버튼을 눌러 병합을 선택하면 됩니다. 그러면 calibre가 파일을 병합하면서 병합된 파일에 대한 모든 링크와 참조도 자동으로 옮겨 줍니다. 단, 개별 파일이 서로 다른 스타일시트를 사용했을 수 있으므로 파일을 병합하면 텍스트 스타일이 바뀔 수 있다는 점에 유의하십시오.

텍스트 파일을 선택한 뒤 다른 텍스트 파일 위로 끌어다 놓아, 끌어놓은 텍스트 파일을 대상 텍스트 파일에 병합할 수도 있습니다.

텍스트 파일 순서 변경

책을 읽을 때 텍스트(HTML) 파일이 열리는 순서는 파일 탐색기`에서 파일을 끌어다 놓거나, 이동할 파일을 클릭한 뒤 :kbd:`Ctrl+Shift`와 :kbd:`위, 아래, Home, End 키를 함께 눌러 다시 정렬할 수 있습니다. 기술적으로는 이를 책의 spine 순서를 다시 정렬한다고 합니다.

항목은 다른 항목 *사이*에 놓아야 하며, 다른 항목 위에 바로 놓으면 안 됩니다. 익숙해지기 전에는 조금 까다롭게 느껴질 수 있습니다. 다른 파일 위에 놓으면 파일이 병합됩니다.

표지 지정

전자책에는 일반적으로 표지 이미지가 있습니다. 이 이미지는 :guilabel:`파일 탐색기`에서 이미지 이름 옆에 갈색 책 아이콘으로 표시됩니다. 다른 이미지를 표지로 지정하려면 파일을 마우스 오른쪽 버튼으로 클릭하고 :guilabel:`표지로 표시하기`를 선택하면 됩니다.

또한 EPUB 파일에는 *표지 페이지(titlepage)*라는 개념이 있습니다. 표지 페이지는 책의 제목 페이지/표지 역할을 하는 HTML 파일입니다. EPUB을 편집할 때는 마우스 오른쪽 버튼 메뉴를 사용해 HTML 파일을 표지 페이지로 지정할 수 있습니다. 단, 지정하는 파일에는 표지 정보만 들어 있어야 한다는 점에 주의하십시오. 만약 첫 장 같은 다른 내용이 들어 있다면, 사용자가 나중에 calibre에서 EPUB 파일을 다른 형식으로 변환할 때 그 내용이 사라질 수 있습니다. 변환 시 calibre는 지정된 표지 페이지에 표지만 있고 다른 내용은 없다고 가정하기 때문입니다.

파일 삭제

파일은 마우스 오른쪽 버튼 메뉴를 사용하거나 선택한 뒤 Delete 키를 눌러 삭제할 수 있습니다. 파일을 삭제하면 OPF 파일에서 해당 파일에 대한 모든 참조가 제거되므로 그 수고를 덜 수 있습니다. 다만 다른 위치의 참조는 제거되지 않으므로, 책 검사 도구를 사용해 쉽게 찾아 제거하거나 바꿀 수 있습니다.

파일 내보내기

책 내부의 파일을 컴퓨터의 다른 위치로 내보낼 수 있습니다. 이는 전문 도구를 사용해 파일만 따로 작업하고 싶을 때 유용합니다. 이렇게 하려면 파일을 마우스 오른쪽 버튼으로 클릭하고 :guilabel:`내보내기`를 선택하십시오.

내보낸 파일 작업을 마쳤다면, 파일을 다시 마우스 오른쪽 버튼으로 클릭하고 :guilabel:`파일로 바꾸기…`를 선택해 책 안으로 다시 가져올 수 있습니다. 그러면 앞서 내보낸 파일로 책 속의 해당 파일을 교체할 수 있습니다.

여러 편집기 인스턴스 사이에서 파일을 복사할 수도 있습니다. :guilabel:`파일 탐색기`에서 복사할 파일을 선택한 다음 마우스 오른쪽 버튼을 눌러 :guilabel:`선택한 파일을 다른 편집기 인스턴스로 복사`를 선택하십시오. 그런 다음 다른 편집기 인스턴스의 :guilabel:`파일 탐색기`에서 마우스 오른쪽 버튼을 눌러 :guilabel:`다른 편집기 인스턴스에서 파일 붙여넣기`를 선택하면 됩니다.

새 이미지/글꼴/기타 추가 또는 새 빈 파일 만들기

컴퓨터에 있는 새 이미지, 글꼴, 스타일시트 등을 파일 → 새 파일`을 클릭해 책에 추가할 수 있습니다. 여기서는 :guilabel:`리소스 파일 가져오기 버튼을 눌러 파일을 가져오거나, 새 파일 이름 상자에 이름을 입력해 빈 HTML 파일이나 스타일시트를 새로 만들 수 있습니다.

파일->책으로 파일 가져오기를 사용하면 여러 파일을 한 번에 책으로 가져올 수도 있습니다.

파일 바꾸기

파일을 마우스 오른쪽 버튼으로 클릭하고 바꾸기를 선택하면 책 안의 기존 파일을 쉽게 교체할 수 있습니다. 교체 파일의 이름이 기존 파일과 다르더라도, 모든 링크와 참조가 자동으로 업데이트됩니다.

HTML 파일에 스타일시트 효율적으로 연결하기

편의상 파일 탐색기에서 여러 HTML 파일을 선택하고 마우스 오른쪽 버튼을 클릭한 다음 스타일시트 연결을 선택하여 캘리버가 선택한 모든 HTML 파일에 해당 스타일시트에 대한 태그를 자동으로 삽입하도록 할 수 있습니다.

검색 & 교체

책 편집에는 현재 파일, 모든 파일, 현재 파일의 표시된 영역 등에서 텍스트를 검색하고 바꿀 수 있는 매우 강력한 검색 및 바꾸기 인터페이스가 있습니다. 일반 검색 또는 정규식을 사용해 검색할 수 있습니다. 고급 검색에 정규식을 사용하는 방법은 :ref:`regexptutorial`을 참조하십시오.

검색 및 바꾸기는 검색 → 찾기/바꾸기 메뉴 항목에서 시작할 수 있습니다. 이 기능을 사용하려면 HTML 또는 CSS 파일을 편집 중이어야 합니다.

찾을 텍스트는 찾기 상자에, 바꿀 텍스트는 바꾸기 상자에 입력하십시오. 그런 다음 적절한 버튼을 눌러 다음 일치를 찾거나, 현재 일치를 바꾸거나, 모든 일치를 한꺼번에 바꿀 수 있습니다.

상자 아래쪽의 드롭다운을 사용하면 현재 파일, 모든 텍스트 파일, 모든 스타일 파일, 또는 모든 파일을 대상으로 검색할 수 있습니다. 검색 모드는 일반(문자열) 검색과 정규식 검색 중에서 선택할 수 있습니다.

검색 식의 모든 일치 개수는 :guilabel:`검색->모두 개수 세기`로 확인할 수 있습니다. 개수 세기는 드롭다운 상자에서 현재 선택한 파일/영역 전체를 대상으로 실행됩니다.

현재 열려 있는 편집기에서 특정 줄로 이동하려면 :guilabel:`검색->줄로 이동`을 사용하십시오.

참고

검색 및 바꾸기의 강력한 기능을 제대로 활용하려면 정규식을 사용해야 합니다. :ref:`regexptutorial`을 참조하십시오.

저장된 검색

자주 사용하는 검색/바꾸기 식(함수 모드 식 포함)은 저장해 두고 여러 번 재사용할 수 있습니다. 검색을 저장하려면 찾기 상자에서 마우스 오른쪽 버튼을 눌러 :guilabel:`현재 검색 저장`을 선택하면 됩니다.

저장된 검색은 검색 → 저장된 검색`에서 불러올 수 있습니다. 그러면 적용할 수 있는 검색 및 바꾸기 식 목록이 표시됩니다. :kbd:`Ctrl 키를 누른 채 항목을 클릭하면 목록에서 여러 항목을 선택해 한 번에 여러 검색/바꾸기 식을 실행할 수도 있습니다.

함수 모드

함수 모드에서는 모든 찾기/바꾸기 작업에 대해 실행되는 매우 강력한 Python 함수를 직접 작성할 수 있습니다. 함수 모드에서는 사실상 원하는 거의 모든 텍스트 조작을 수행할 수 있습니다. 자세한 내용은 :doc:`function_mode`를 참조하십시오.

HTML 태그를 무시하고 검색

중간에 있는 HTML 태그를 무시하고 텍스트를 검색하는 전용 도구도 있습니다. 예를 들어 책에 Emphasis on a <i>word</i>.``라는 HTML이 있다면, 중간에 ``<i> 태그가 있어도 ``on a word``를 검색해 찾을 수 있습니다. 이 도구는 검색 → HTML 마크업 무시하고 검색 메뉴 항목으로 사용할 수 있습니다.

자동화 도구

책 편집에는 일반적인 작업을 돕는 다양한 도구가 있습니다. 이 도구들은 도구 메뉴에서 사용할 수 있습니다.

목차 편집

목차 편집을 쉽게 해 주는 전용 도구가 있습니다. :guilabel:`도구->목차->목차 편집`으로 실행하십시오.

목차 편집 도구는 현재 목차(있는 경우)를 왼쪽에 보여 줍니다. 항목을 두 번 클릭하면 텍스트를 바꿀 수 있습니다. 끌어서 놓기나 오른쪽의 버튼을 사용해 항목 순서를 다시 정렬할 수도 있습니다.

기존 목차가 없는 책의 경우, 이 도구는 텍스트에서 목차를 자동으로 생성하는 여러 옵션을 제공합니다. 문서의 제목, 링크, 개별 파일 등을 기준으로 생성할 수 있습니다.

개별 항목은 해당 항목을 클릭한 다음 이 항목이 가리키는 위치 변경 버튼을 눌러 편집할 수 있습니다. 그러면 책의 미니 미리보기가 열립니다. 책 보기 패널 위로 마우스 커서를 움직여 원하는 위치를 클릭하면 됩니다. 굵은 녹색 선이 위치를 표시해 줍니다. 원하는 위치가 맞으면 확인을 누르십시오.

책 검사

책 검사 도구는 실제 리더 장치에서 책이 의도한 대로 동작하지 못하게 할 수 있는 문제를 검색합니다. :guilabel:`도구->책 검사`로 실행하십시오.

발견된 문제는 사용하기 쉬운 목록으로 보기 좋게 보고됩니다. 목록의 항목을 클릭하면 해당 오류에 대한 도움말이 표시되며, 자동으로 수정할 수 있는 오류라면 자동 수정 옵션도 제공됩니다. 오류를 두 번 클릭하면 편집기에서 오류 위치를 열어 직접 수정할 수도 있습니다.

수행되는 검사 중 일부는 다음과 같습니다.

- 잘못된 HTML 마크업. 올바른 XML 형식으로 해석되지 않는 HTML 마크업은 모두 보고됩니다. 이를 수정하면 모든 환경에서 의도한 대로 마크업이 동작하도록 할 수 있습니다. calibre는 이런 오류를 자동으로 고칠 수도 있지만, 자동 수정은 때때로 예상 밖의 결과를 낼 수 있으므로 주의해서 사용하십시오. 늘 그렇듯 자동 수정 전에 체크포인트가 만들어지므로 모든 변경 사항을 쉽게 되돌릴 수 있습니다. 자동 수정은 HTML5 알고리즘(오류 허용성이 매우 높은 방식)으로 마크업을 해석한 뒤, 이를 올바른 XML로 변환하는 방식으로 동작합니다.

- 잘못되었거나 알 수 없는 CSS 스타일. 유효하지 않거나 CSS 2.1 표준(및 일부 CSS 3 속성)에 정의되지 않은 속성을 사용하는 CSS는 모두 보고됩니다. CSS는 모든 스타일시트, 인라인 style 속성, HTML 파일 안의 <style> 태그에서 검사됩니다.

- 끊어진 링크. 책 내부의 누락된 파일을 가리키는 링크를 보고합니다.

- 참조되지 않는 파일. 책 안에 있지만 다른 파일에서 참조되지 않거나 spine에 포함되지 않은 파일을 보고합니다.

- 중복된 spine 또는 manifest 항목, 잘못된 idref나 메타 표지 태그, 필수 구역 누락 등 OPF 파일의 다양한 일반 문제.

- 리더 장치에서 책이 제대로 동작하지 않을 수 있는 알려진 문제를 검사하는 다양한 호환성 검사.

표지 추가

:guilabel:`도구->표지 추가하기`를 통해 책에 표지를 쉽게 추가할 수 있습니다. 이렇게 하면 책에 있는 기존 이미지를 표지로 선택하거나 새 이미지를 책으로 가져와 표지로 만들 수 있습니다. EPUB 파일을 편집할 때 표지용 HTML 래퍼가 자동으로 생성됩니다. 책의 기존 표지가 발견되면 교체됩니다. 이 도구는 또한 자동으로 표지 파일을 OPF의 표지로 올바르게 표시합니다.

참조된 글꼴 포함하기

:guilabel:`도구->참조 글꼴 포함`에서 사용할 수 있는 이 도구는 책에서 참조하는 모든 글꼴을 찾고, 아직 포함되어 있지 않다면 컴퓨터에서 해당 글꼴을 찾아 책 안에 포함합니다. 단, 상용 라이선스 글꼴을 포함하기 전에 필요한 저작권/라이선스 권한을 갖추었는지 반드시 확인하십시오.

내장된 글꼴의 사용된 문자만 취하기 (Subsetting embedded fonts)

:guilabel:`도구->포함된 글꼴 서브셋 만들기`에서 사용할 수 있는 이 도구는 책의 모든 글꼴을 실제로 책 안에 मौजूद한 텍스트에 필요한 글리프만 포함하도록 줄여 줍니다. 일반적으로 글꼴 파일 크기를 약 50% 줄일 수 있습니다. 다만 글꼴을 서브셋으로 만들고 난 뒤 이전에 없던 문자를 포함한 새 텍스트를 추가하면, 해당 새 텍스트에는 글꼴이 동작하지 않을 수 있습니다. 따라서 이 작업은 작업 흐름의 마지막 단계에서만 수행하는 것이 좋습니다.

지능적 문장부호 처리

일반 텍스트의 대시, 줄임표, 따옴표, 여러 개의 하이픈 등을 올바른 조판 기호로 변환합니다. 단축형 단어 앞의 작은따옴표가 포함된 경우처럼 알고리즘이 때때로 잘못된 결과를 낼 수 있다는 점에 유의하십시오. :guilabel:`도구->구두점 스마트하게 바꾸기`에서 사용할 수 있습니다.

CSS 속성 변형하는 중

책의 스타일을 변환하는 규칙을 만듭니다. 예를 들어 빨간색 텍스트를 모두 초록색으로 바꾸거나, 책 안의 모든 텍스트 글꼴 크기를 두 배로 늘리거나, 특정 글꼴 계열의 텍스트를 기울임꼴로 만드는 규칙 등을 만들 수 있습니다.

규칙 작성은 간단하며, 규칙은 다음과 같은 자연어 형식을 따릅니다.

- 속성 color*가 *red*이면 *green*으로 *변경

- 속성 font-size*가 *임의의 값*이면 그 값을 *2*배로 *곱함

:guilabel:`도구->스타일 변환`에서 사용할 수 있습니다.

사용하지 않는 CSS 규칙 제거 중

스타일시트와 <style> 태그에서 사용되지 않는 CSS 규칙을 모두 제거합니다. 제작용 템플릿에서 만든 일부 책에는 실제 내용과 맞지 않는 불필요한 CSS 규칙이 많이 들어 있을 수 있습니다. 이런 불필요한 규칙은 모두 처리해야 하는 리더의 속도를 늦출 수 있습니다. :guilabel:`도구->사용되지 않는 CSS 제거`에서 사용할 수 있습니다.

HTML 수정

이 도구는 XML로 해석할 수 없는 HTML을 올바른 형식의 XML로 변환합니다. 전자책에는 올바른 XML 형식이 아닌 HTML이 들어 있는 경우가 매우 흔하므로, 이 도구는 그런 HTML을 고치는 과정을 자동화해 줍니다. HTML5 알고리즘(모든 현대 브라우저가 사용하는 알고리즘)으로 HTML을 해석한 뒤 그 결과를 XML로 변환하는 방식으로 동작합니다. 다만 자동 수정은 때때로 직관과 다른 결과를 낼 수 있다는 점을 기억하십시오. 원한다면 앞서 설명한 책 검사 도구를 사용해 HTML의 문제를 찾아 수동으로 수정할 수도 있습니다. :guilabel:`도구->HTML 수정`에서 사용할 수 있습니다.

파일 서식 정리

이 도구는 모든 HTML 및 CSS 파일을 자동으로 서식화하여 “보기 좋게” 만듭니다. 코드는 들여쓰기가 자동으로 맞춰지고, 필요한 곳에 빈 줄이 삽입되는 등 정리됩니다. 파일 서식 정리에는 깨진 HTML/CSS 자동 수정도 포함된다는 점에 유의하십시오. 따라서 자동 수정을 원하지 않는다면 먼저 책 검사 도구로 모든 문제를 고친 다음 그 후에 서식 정리를 실행하십시오. :guilabel:`도구->모든 파일 서식 정리`에서 사용할 수 있습니다.

참고

HTML에서는 CSS의 white-space 지시어에 따라 어떤 텍스트든 공백이 의미를 가질 수 있습니다. 따라서 서식 정리는 잠재적으로 HTML의 렌더링 결과를 바꿀 수 있습니다. 이를 최대한 피하기 위해 서식 정리 알고리즘은 다른 블록 수준 태그를 포함하는 블록 수준 태그만 정리합니다. 예를 들어 <p> 태그 내부 텍스트의 공백은 바꾸지 않습니다. 하지만 <body> 태그 안에 <p>, <div> 같은 블록 태그만 들어 있다면 서식 정리가 적용됩니다. 그래서 적절한 블록 수준 태그가 없는 파일은 서식 정리의 영향을 받지 않을 수도 있습니다. 이런 경우에는 좀 더 덜 신중한 다른 정리 도구를 시도해 볼 수 있습니다. 예를 들어 HTML Tidy [https://infohound.net/tidy/] 같은 도구가 있습니다.

본문 안에 목차 삽입

일반적으로 전자책에서 목차는 본문과 분리되어 있으며, 보통 전자책 읽기 장치의 전용 목차 버튼/메뉴를 통해 접근합니다. calibre를 사용하면 본문 일부가 되는 인라인 목차를 자동으로 생성할 수도 있습니다. 이 목차는 현재 정의된 목차를 바탕으로 만들어집니다.

이 도구를 여러 번 사용하면 실행할 때마다 이전에 만들어진 인라인 목차가 새 것으로 바뀝니다. 이 도구는 :guilabel:`도구->목차->본문 안에 목차 삽입`에서 사용할 수 있습니다.

의미 정보 설정

이 도구는 EPUB 파일에서 *의미 정보(semantics)*를 설정하는 데 사용됩니다. 의미 정보란 간단히 말해, 책 안의 특정 위치가 특별한 의미를 가진다는 것을 식별해 주는 OPF 파일의 링크입니다. 이를 사용하면 머리말, 헌사, 표지, 목차 등을 지정할 수 있습니다. 지정하려는 의미 정보의 종류를 선택한 다음, 링크가 가리켜야 할 책 속 위치를 선택하면 됩니다. 이 도구는 :guilabel:`도구->의미 정보 설정`에서 사용할 수 있습니다.

스타일 정보 필터링

이 도구를 사용하면 지정한 CSS 스타일 속성을 책 전체에서 손쉽게 제거할 수 있습니다. 예를 들어 color, background-color, line-height 같은 속성을 지정하면 스타일시트, <style> 태그, 인라인 style 속성 등 해당 속성이 나타나는 모든 곳에서 제거합니다. 스타일 정보를 제거한 뒤에는 변경된 모든 내용의 요약이 표시되어 정확히 무엇이 바뀌었는지 확인할 수 있습니다. 이 도구는 :guilabel:`도구->스타일 정보 필터링`에서 사용할 수 있습니다.

책 내부 구조 업그레이드

가능한 경우 이 도구를 사용해 책의 내부 구조를 업그레이드할 수 있습니다. 예를 들어 EPUB 2 책을 EPUB 3 책으로 업그레이드합니다. 이 도구는 :guilabel:`책 내부 구조 업그레이드`에서 사용할 수 있습니다.

체크포인트

:guilabel:`체크포인트`는 책의 현재 상태를 “특별한 상태”로 표시하는 방법입니다. 이후 책에 원하는 변경을 계속한 다음 결과가 마음에 들지 않으면 체크포인트를 만든 상태로 돌아갈 수 있습니다. 체크포인트는 앞 절에서 설명한 자동화 도구를 실행할 때마다 자동으로 만들어집니다.

체크포인트는 :guilabel:`편집->체크포인트 만들기`로 만들 수 있습니다. 이전 체크포인트로 돌아가려면 :guilabel:`편집->되돌리기 …`를 사용하십시오.

체크포인트 기능은 개별 파일을 편집할 때의 일반적인 실행 취소/다시 실행 기능에 추가로 제공됩니다. 변경 사항이 책의 여러 파일에 걸쳐 있거나, 서로 관련된 큰 변경 묶음을 한 번에 되돌리고 싶을 때 체크포인트가 필요합니다.

사용 가능한 체크포인트 목록은 보기 → 체크포인트`에서 확인할 수 있습니다. 관심 있는 체크포인트를 선택하고 :guilabel:`비교 버튼을 클릭하면 전자책 비교 도구를 사용해 현재 책 상태와 해당 체크포인트를 비교할 수 있습니다. 되돌리기 버튼을 누르면 해당 체크포인트가 만들어진 이후의 모든 변경 사항을 취소하고 책을 선택한 체크포인트 상태로 복원합니다.

실시간 미리보기 패널

실시간 미리보기 패널은 현재 수행 중인 변경 사항을 실시간으로 보여 줍니다(1~2초 정도 지연될 수 있음). HTML 또는 CSS 파일을 편집하면 변경 사항이 반영되도록 미리보기 패널이 자동으로 업데이트됩니다. 편집기에서 커서를 움직이면 미리보기 패널도 그 위치를 따라가며 책에서 대응되는 위치를 보여 줍니다. 미리보기 패널을 클릭하면 편집기의 커서가 클릭한 요소 위로 이동합니다. 책 안의 다른 파일을 가리키는 링크를 클릭하면 해당 파일이 편집기와 미리보기 패널에서 자동으로 열립니다.

미리보기 패널 아래의 버튼을 사용하면 위치 자동 동기화와 변경 사항의 실시간 미리보기를 끌 수 있습니다. 미리보기 패널은 편집기에서 활발히 타이핑 중이 아닐 때만 실시간으로 갱신되므로, 렌더링을 기다리느라 방해받거나 작업 속도가 느려지지 않습니다.

미리보기 패널은 텍스트가 실제로 어떻게 보일지 보여 줍니다. 하지만 미리보기 패널은 실제 리더 장치에서 책을 시험해 보는 것을 대신할 수는 없습니다. 실제 리더보다 더 잘하는 부분도 있고 덜하는 부분도 있습니다. 대부분의 리더 장치보다 오류나 느슨한 마크업을 더 잘 허용하기도 합니다. 반대로 페이지 여백, 페이지 나눔, 글꼴 이름 별칭을 사용하는 내장 글꼴 등은 보여 주지 못합니다. 작업 중에는 미리보기 패널을 활용하되, 작업을 마친 뒤에는 실제 리더 장치나 소프트웨어 에뮬레이터에서 다시 검토하십시오.

참고

글꼴 파일 내부의 글꼴 이름이 CSS @font-face 규칙의 이름과 일치하지 않으면 미리보기 패널에서는 해당 내장 글꼴을 지원하지 않습니다. 이런 문제 글꼴은 책 검사 도구를 사용해 빠르게 찾아 수정할 수 있습니다.

HTML 파일 분할

미리보기 패널의 잘 알려지지 않은 유용한 활용법 중 하나는 긴 HTML 파일을 분할하는 것입니다. 분할하려는 파일을 보고 있는 상태에서 미리보기 패널 아래의 분할 모드 버튼 |spmb|을 클릭하십시오. 그런 다음 파일을 나누고 싶은 위치로 마우스를 움직여 클릭하면 됩니다. 마우스를 움직이는 동안 굵은 녹색 선이 분할이 일어날 정확한 위치를 보여 줍니다. 원하는 위치를 찾으면 클릭하기만 하면 분할이 수행됩니다.

파일을 분할하면 파일의 아래쪽 절반을 가리키던 모든 링크와 참조가 자동으로 업데이트되며, 새로 분할된 파일이 편집기에서 열립니다.

편집기에서 파일 내부를 마우스 오른쪽 버튼으로 클릭하고 :guilabel:`여러 위치에서 분할`을 선택하면, 하나의 HTML 파일을 여러 위치에서 자동으로 분할할 수도 있습니다. 이를 이용하면 큰 파일을 모든 제목 태그 위치나 특정 클래스를 가진 모든 태그 위치에서 쉽게 나눌 수 있습니다.

실시간 CSS 패널

실시간 CSS 패널은 현재 편집 중인 태그에 적용되는 모든 스타일 규칙을 보여 줍니다. 편집기에서의 줄 번호와 함께 태그 이름이 표시되고, 그 아래에 일치하는 스타일 규칙 목록이 표시됩니다.

어떤 태그에 어떤 스타일 규칙이 적용되는지 빠르게 확인할 수 있는 훌륭한 방법입니다. 이 보기에는 클릭 가능한 링크(파란색)도 있으며, 스타일 규칙을 수정하고 싶을 때 해당 스타일이 정의된 위치로 바로 이동할 수 있습니다. 태그에 직접 적용되는 규칙뿐 아니라 상위 태그에서 상속된 규칙도 함께 표시됩니다.

이 패널은 해당 태그에 대해 최종적으로 계산된 스타일도 보여 줍니다. 우선순위가 더 높은 규칙에 의해 덮어쓰인 속성은 목록에서 취소선으로 표시됩니다.

실시간 CSS 패널은 :guilabel:`보기->실시간 CSS`에서 켤 수 있습니다.

기타 도구

책을 편집하는 동안 유용하게 쓸 수 있는 도구가 몇 가지 더 있습니다.

목차 보기

목차 보기는 책의 현재 목차를 보여 줍니다. 항목을 두 번 클릭하면 해당 항목이 가리키는 위치가 편집기에서 열립니다. 마우스 오른쪽 버튼을 눌러 목차를 편집하거나, 보기를 새로 고치거나, 모든 항목을 펼치거나 접을 수 있습니다. 이 보기는 :guilabel:`보기->목차`에서 열 수 있습니다.

책에 있는 단어의 철자 검사

:guilabel:`도구->철자 검사`를 통해 철자 검사기를 실행할 수 있습니다.

단어는 책에 나타나는 횟수와 해당 단어가 속한 언어와 함께 표시됩니다. 언어 정보는 책의 메타데이터와 HTML 파일의 lang 속성에서 가져옵니다. 덕분에 여러 언어의 텍스트가 섞여 있는 책에서도 맞춤법 검사기가 잘 동작합니다. 예를 들어 아래 HTML 예제에서 color라는 단어는 미국 영어로, colour라는 단어는 영국 영어로 검사됩니다.

<div lang="en_US">color <span lang="en_GB">colour</span></div>

참고

단어를 두 번 클릭하면 편집기에서 그 단어의 다음 출현 위치가 강조 표시됩니다. 단어를 직접 수정하거나 어떤 문맥에서 쓰였는지 보고 싶을 때 유용합니다.

단어를 변경하려면 오른쪽에 제안된 대체 철자 중 하나를 두 번 클릭하거나, 나만의 수정한 철자를 직접 입력하고 선택한 단어를 다음으로 변경 버튼을 클릭하십시오. 이것은 책에 나오는 모든 단어를 대체합니다. 또한 메인 단어 목록에서 단어를 오른쪽 클릭하면 오른쪽 클릭 메뉴에서 편리하게 단어를 변경할 수 있습니다.

Ignore 버튼을 클릭하여 철자 검사기가 현재 세션의 단어를 무시하도록 할 수 있습니다. 사전에 추가하기 버튼을 클릭하여 사용자 사전에 단어를 추가할 수도 있습니다. 철자 검사기는 여러 사용자 사전을 지원하므로 단어를 추가할 사전을 선택할 수 있습니다.

철자 검사기가 철자가 잘못된 단어뿐만 아니라 책의 모든 단어를 표시하도록 할 수도 있습니다. 이것은 책에서 가장 많이 사용되는 단어를 확인하고 간단한 검색을 실행하고 개별 단어를 바꿀 때 유용합니다.

참고

맞춤법 검사 도구를 열어 둔 상태에서 파일을 편집해 책을 변경했다면, 맞춤법 검사 도구의 새로 고침 버튼을 클릭해야 합니다. 그렇게 하지 않고 계속 맞춤법 검사 도구를 사용하면 편집기에서 한 변경 사항을 잃을 수 있습니다.

참고

맞춤법 검사 도구를 실행할 때 특정 파일을 검사 대상에서 제외하려면 파일 제외 버튼을 사용하거나, 해당 파일의 시작 태그 바로 아래에 다음 주석을 추가하십시오.

<!-- calibre-no-spell-check -->

새 사전 추가

철자 검사기에는 영어 및 스페인어 사전이 내장된되어 있습니다. :guilabel:`환경설정->편집기->철자 사전 관리`를 통해 나만의 사전을 설치할 수 있습니다. 철자 검사기는 LibreOffice 프로그램의 사전(.oxt 형식)을 사용할 수 있습니다. 이 사전은 `LibreOffice Extensions 저장소 <https://extensions.libreoffice.org/?Tags%5B%5D=50>`_에서 다운로드할 수 있습니다.

특수 문자 삽입

편집 → 특수 문자 삽입 도구를 사용하면 입력하기 어려운 문자를 넣을 수 있습니다. 이 도구는 모든 유니코드 문자를 보여 주며, 입력하려는 문자를 클릭하기만 하면 됩니다. 클릭할 때 Ctrl 키를 누르고 있으면 선택한 문자를 넣은 뒤 창이 자동으로 닫힙니다. 이 도구는 본문뿐 아니라 검색 및 바꾸기 도구 같은 사용자 인터페이스의 다른 영역에도 특수 문자를 삽입할 때 사용할 수 있습니다.

문자가 아주 많기 때문에, 먼저 표시될 나만의 즐겨찾기 문자를 지정할 수 있습니다. 문자를 마우스 오른쪽 버튼으로 클릭하면 즐겨찾기로 표시됩니다. 즐겨찾기에 있는 문자도 마우스 오른쪽 버튼으로 클릭해 즐겨찾기에서 제거할 수 있습니다. 마지막으로 즐겨찾기 재배열 버튼을 클릭한 뒤 끌어서 놓아 즐겨찾기 문자의 순서를 바꿀 수 있습니다.

키보드를 사용해 특수 문자를 직접 입력할 수도 있습니다. 방법은 문자의 유니코드 코드(16진수)를 입력한 뒤 Alt+X 키를 누르는 것입니다. 그러면 방금 입력한 코드가 해당 문자로 변환됩니다. 예를 들어 ÿ를 입력하려면 ff를 입력한 다음 Alt+X`를 누르면 됩니다. 줄바꿈 없는 공백은 a0 다음 :kbd:`Alt+X, 가로 줄임표는 2026 다음 :kbd:`Alt+X`를 사용하면 됩니다.

마지막으로 HTML 이름 엔터티를 사용해 특수 문자를 입력할 수도 있습니다. 예를 들어 &nbsp;를 입력한 뒤 세미콜론을 치면 줄바꿈 없는 공백으로 바뀝니다. 이 치환은 세미콜론을 입력하는 순간에만 일어납니다.

코드 검사기 보기

이 보기는 현재 관심 있는 요소에 적용되는 HTML 코드와 CSS를 보여 줍니다. 미리보기 패널의 위치를 마우스 오른쪽 버튼으로 클릭한 뒤 :guilabel:`검사`를 선택하면 열 수 있습니다. 이 보기를 사용하면 해당 요소의 HTML 코드와, 더 중요하게는 그 요소에 적용되는 CSS 스타일을 확인할 수 있습니다. 스타일을 동적으로 편집해 변경 효과를 즉시 확인할 수도 있습니다. 다만 여기서 스타일을 편집해도 실제 책 내용이 바뀌는 것은 아니며, 빠른 실험을 위한 기능일 뿐입니다. 검사기 내부의 실시간 편집 기능은 현재 개발 중입니다.

외부 링크 검사

이 도구를 사용하면 책 안에서 외부 웹사이트를 가리키는 모든 링크를 검사할 수 있습니다. 이 도구는 외부로 연결된 모든 웹사이트에 접속을 시도하며, 접속에 실패하면 끊어진 링크를 수정하기 쉬운 형식으로 보고해 줍니다.

외부 리소스 다운로드 중

이 도구를 사용하면 책에 포함되어 있지 않은 이미지/스타일시트 등의 리소스(즉 인터넷상의 위치를 가리키는 URL을 가진 리소스)를 자동으로 다운로드할 수 있습니다. 이런 리소스를 모두 찾아 자동으로 내려받아 책에 추가하고, 그 리소스를 가리키는 모든 참조를 다운로드한 파일을 사용하도록 바꿉니다.

파일을 형식별 폴더로 정리

종종 어딘가에서 가져온 EPUB 파일을 편집할 때 EPUB 내부의 파일이 다른 하위 폴더에 아무렇게나 배열되어 있음을 알 수 있습니다. 이 도구를 사용하면 모든 파일을 유형에 따라 자동으로 하위 폴더로 이동할 수 있습니다. :guilabel:`도구->폴더에 정렬`을 통해 액세스합니다. 이 도구는 EPUB 내에서 파일이 정렬되는 방식만 변경하며 파일 탐색기에 표시되는 방식은 변경하지 않습니다.

다른 전자책 형식의 파일을 EPUB로 가져오는 중

이 편집기에는 전체 변환 과정을 거치지 않고 다른 전자책 형식의 파일을 바로 새 EPUB으로 가져오는 기능이 있습니다. 이 기능은 손수 편집한 HTML 파일에서 EPUB 파일을 직접 만들 때 특히 유용합니다. :guilabel:`파일->HTML 또는 DOCX 파일을 새 책으로 가져오기`에서 사용할 수 있습니다.

보고서 도구

이 편집기에는 도구 → 보고서`에서 사용할 수 있는 훌륭한 *보고서* 도구가 있어, 책에서 사용된 파일, 이미지, 링크, 단어, 문자, 스타일의 요약을 보여 줍니다. 보고서의 모든 줄은 클릭 가능한 링크입니다. 줄을 두 번 클릭하면 책 안에서 해당 항목이 사용되거나 정의된 위치로 바로 이동합니다. 예를 들어 :guilabel:`링크 보기에서는 원본 열의 항목을 두 번 클릭해 링크가 정의된 위치로 이동하고, 대상 열의 항목을 두 번 클릭해 링크가 가리키는 위치로 이동할 수 있습니다.

코드 편집기의 특수 기능
calibre HTML 편집기는 매우 강력합니다. HTML(및 CSS) 편집을 더 쉽게 해 주는 많은 기능을 갖추고 있습니다.

구문 강조표시

HTML 편집기에는 매우 정교한 구문 강조 기능이 있습니다. 예를 들면 다음과 같습니다.

- 굵게, 기울임꼴, 제목 태그 내부의 텍스트가 굵게/기울임꼴로 표시됩니다.

- HTML 안에서 커서를 움직이면 짝이 맞는 HTML 태그가 강조 표시되며, Ctrl+{ 및 Ctrl+} 단축키를 사용해 여는 태그나 닫는 태그로 바로 이동할 수 있습니다. 비슷하게 Ctrl+Alt+T 또는 :kbd:`Ctrl+Shift+T`로 태그 내부 내용을 선택할 수도 있습니다.

- 잘못된 HTML은 빨간 밑줄로 강조 표시됩니다.

- 제목과 같은 속성 및 HTML 태그 내 텍스트의 철자 오류가 강조 표시됩니다. 철자 검사는 현재 태그의 lang 속성 값과 전체 책 언어를 기반으로 언어를 인식합니다.

- <style> 태그 안에 포함된 CSS가 강조 표시됩니다.

- 줄바꿈 없는 공백, 서로 다른 종류의 하이픈 등 구분하기 어려운 특수 문자도 강조 표시됩니다.

- <a>, <img>, <link> 태그 안에서 다른 파일을 가리키는 링크는 모두 파일명이 강조 표시됩니다. 그리고 그 파일명이 가리키는 대상이 존재하지 않으면 파일명에 빨간 밑줄이 표시됩니다.

문맥에 맞는 도움말

HTML 태그 이름이나 CSS 속성 이름을 마우스 오른쪽 버튼으로 클릭하면 해당 태그나 속성에 대한 도움말을 볼 수 있습니다.

Ctrl 키를 누른 채 링크 태그 안의 파일명을 클릭하면 해당 파일이 편집기에서 자동으로 열립니다. 마찬가지로 클래스 이름을 Ctrl 클릭하면 해당 태그와 클래스에 일치하는 첫 번째 스타일 규칙으로 이동합니다.

HTML 파일에서 클래스 이름을 마우스 오른쪽 버튼으로 클릭하면 클래스 이름을 바꿀 수 있으며, 그러면 책 전체와 모든 스타일시트에 있는 해당 클래스의 모든 출현 위치가 함께 변경됩니다.

자동 완성

전자책을 편집할 때 가장 번거로운 작업 중 하나는 책 안의 다른 파일, CSS 스타일시트, 이미지 등에 대한 링크를 만드는 일입니다. 올바른 파일명과 상대 경로를 직접 알아내야 하기 때문입니다. 편집기에는 이를 더 쉽게 해 주는 자동 완성 기능이 있습니다.

파일명을 입력하기 시작하면 편집기가 자동으로 추천 목록을 표시합니다. Tab 키를 눌러 올바른 파일명을 선택하면 됩니다. 편집기는 다른 HTML 파일 안의 앵커를 가리키는 링크에 대해서도 추천을 제공합니다. # 문자를 입력하면 편집기가 대상 파일의 모든 앵커 목록을 보여 주며, 올바른 앵커를 고를 수 있도록 짧은 텍스트 조각도 함께 표시합니다.

대부분의 다른 자동 완성 시스템과 달리, 편집기의 자동 완성 시스템은 부분 수열 매칭을 사용합니다. 즉 파일명 어디에서든 두세 글자만 입력해도 파일명을 완성할 수 있습니다. 예를 들어 ``../images/arrow1.png``라는 파일명을 원한다면, 그냥 ``ia1``을 입력하고 :kbd:`Tab`을 눌러 완성할 수 있습니다. 일치를 찾을 때 자동 완성 시스템은 단어 시작 부분에 있는 문자나 경로 구분자 바로 뒤에 있는 문자를 우선시합니다. 이 방식에 익숙해지면 시간과 수고를 많이 아낄 수 있다는 것을 알게 될 것입니다.

토막글
calibre 전자책 편집기는 *스니펫*을 지원합니다. 스니펫은 자주 재사용되거나 중복된 텍스트가 많이 들어 있는 텍스트 조각입니다. 편집기에서는 몇 번의 키 입력만으로 스니펫을 삽입할 수 있습니다. 스니펫은 매우 강력해서, 이동 가능한 자리표시자, 반복된 텍스트의 자동 미러링 같은 다양한 기능을 제공합니다. 자세한 내용은 :doc:`snippets`를 참조하십시오.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 전자책 편집하는 중 »
- 편집기에서의 찾아 바꾸기 함수 모드

편집기에서의 찾아 바꾸기 함수 모드

편집기의 찾아 바꾸기 도구는 *함수 모드*를 지원합니다. 이 모드에서는 정규 표현식(calibre에서 정규 표현식 사용하기 참조)을 임의로 강력한 Python 함수와 결합하여 다양한 고급 텍스트 처리를 수행할 수 있습니다.

표준 regexp 모드에서는 검색할 정규 표현식과 발견된 모든 일치 항목을 대체하는 데 사용할 템플릿을 모두 지정합니다. 함수 모드에서는 고정 템플릿 대신 `Python 프로그래밍 언어 <https://docs.python.org>`_로 임의의 함수를 지정합니다. 이를 통해 단순한 템플릿으로는 불가능한 많은 작업을 수행할 수 있습니다.

함수 모드를 사용하는 기술과 구문은 점점 더 복잡한 작업을 수행하는 함수를 만드는 방법을 보여주는 예제를 통해 설명됩니다.

문서의 머리글 대소문자 자동 수정

여기에서는 편집기의 내장 함수 중 하나를 활용하여 머리글 태그 안의 모든 텍스트 대소문자를 제목 대소문자로 자동 변경합니다:

Find expression: <([Hh][1-6])[^>]*>.+?</\1>

함수로 제목 대소문자 텍스트 (태그 무시) 내장 함수를 선택하기만 하면 됩니다. 이렇게 하면 ``<h1>some titLE</h1>``과 같은 제목이 ``<h1>Some Title</h1>``로 변경됩니다. 머리글 태그 안에 다른 HTML 태그가 있어도 작동합니다.

첫 번째 사용자 지정 함수 - 하이픈 스마트 변환

함수 모드의 진정한 힘은 임의의 방식으로 텍스트를 처리하는 자신만의 함수를 만들 수 있다는 것입니다. 편집기의 스마트 문장 부호 도구는 개별 하이픈을 그대로 두므로, 이 함수를 사용하여 하이픈을 엄선으로 대체할 수 있습니다.

새 함수를 만들려면 만들기/편집하기 버튼을 클릭하여 새 함수를 만들고 아래의 Python 코드를 복사하세요.

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
return match.group().replace('--', '—').replace('-', '—')

모든 찾아 바꾸기 사용자 지정 함수는 고유한 이름을 가져야 하며, 위에 표시된 모든 인수를 받는 replace라는 이름의 Python 함수로 구성되어야 합니다. 지금은 replace() 함수의 다양한 인수에 대해 걱정하지 않겠습니다. match 인수에만 집중하세요. 찾아 바꾸기를 실행할 때 일치 항목을 나타냅니다. 전체 문서는 `여기 <https://docs.python.org/library/re.html#match-objects>`_에서 확인할 수 있습니다. ``match.group()``은 일치한 모든 텍스트를 반환하며, 해당 텍스트의 하이픈을 엄선으로 대체합니다. 먼저 이중 하이픈을 대체한 다음 단일 하이픈을 대체합니다.

이 함수를 다음 찾기 정규 표현식과 함께 사용하세요:

>[^<>]+<

그러면 모든 하이픈이 엄선으로 대체되지만, 실제 텍스트에서만 대체되고 HTML 태그 정의 안에서는 대체되지 않습니다.

함수 모드의 강력함 - 철자 사전을 사용하여 하이픈이 잘못된 단어 수정

종이책 스캔으로 만든 전자책에는 종종 하이픈이 잘못된 단어, 즉 인쇄된 페이지의 줄 끝에서 분할된 단어가 포함되어 있습니다. 이러한 단어를 자동으로 찾아 수정하는 간단한 함수를 작성하겠습니다.

import regex
from calibre import replace_entities
from calibre import prepare_string_for_xml

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):

def replace_word(wmatch):
# Try to remove the hyphen and replace the words if the resulting
# hyphen free word is recognized by the dictionary
without_hyphen = wmatch.group(1) + wmatch.group(2)
if dictionaries.recognized(without_hyphen):
return without_hyphen
return wmatch.group()

# Search for words split by a hyphen
text = replace_entities(match.group()[1:-1]) # Handle HTML entities like &amp;
corrected = regex.sub(r'(\w+)\s*-\s*(\w+)', replace_word, text, flags=regex.VERSION1 | regex.UNICODE)
return '>%s<' % prepare_string_for_xml(corrected) # Put back required entities

이 함수를 이전과 동일한 찾기 표현식과 함께 사용하세요:

>[^<>]+<

그러면 책 텍스트의 하이픈이 잘못된 모든 단어가神奇하게 수정됩니다. 핵심은 대체 함수의 유용한 추가 인수 중 하나인 ``dictionaries``를 사용하는 것입니다. 이는 편집기 자체가 책의 텍스트 철자 검사에 사용하는 사전을 나타냅니다. 이 함수는 하이픈으로 분리된 단어를 찾아 하이픈을 제거하고 사전이 합성어를 인식하는지 확인합니다. 인식하면 원래 단어가 하이픈 없는 합성어로 대체됩니다.

이 기술의 한 가지 제한 사항은 기본적으로 ``dictionaries.recognized()``가 책의 주 언어를 사용하므로 단일 언어 책에서만 작동한다는 점입니다.

섹션 자동 번호 매기기

이제 약간 다른 것을 살펴보겠습니다. HTML 파일에 여러 섹션이 있고 각 섹션에 <h2>Some text</h2>`와 같은 :code:`<h2> 태그의 머리글이 있다고 가정합니다. 이러한 머리글에 연속적인 섹션 번호를 자동으로 매기는 사용자 지정 함수를 만들어 :code:`<h2>1. Some text</h2>`처럼 보이게 할 수 있습니다.

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
section_number = '%d. ' % number
return match.group(1) + section_number + match.group(2)

# Ensure that when running over multiple files, the files are processed
# in the order in which they appear in the book
replace.file_order = 'spine'

다음 찾기 표현식과 함께 사용하세요:

(?s)(<h2[^<>]*>)(.+?</h2>)

커서를 파일 상단에 놓고 :guilabel:`모두 바꾸기`를 클릭하세요.

이 함수는 replace()``의 또 다른 유용한 추가 인수인 ``number 인수를 사용합니다. :guilabel:`모두 바꾸기`를 수행할 때 number는 각 연속 일치 항목에 대해 자동으로 증가합니다.

또 다른 새로운 기능은 ``replace.file_order``의 사용입니다. 이를 ``’spine’``으로 설정하면 여러 HTML 파일에서 이 검색이 실행될 때 파일이 책에 나타나는 순서대로 처리됩니다. 자세한 내용은 :ref:`file_order_replace_all`을 참조하세요.

목차 자동 생성

마지막으로 좀 더 도전적인 것을 시도해 보겠습니다. 책에 <h1 id="someid">Some Text</h1>``처럼 보이는 ``h1 및 h2 태그의 머리글이 있다고 가정합니다. 이러한 머리글을 기반으로 HTML 목차를 자동 생성하겠습니다. 아래의 사용자 지정 함수를 만드세요:

from calibre import replace_entities from calibre.ebooks.oeb.polish.toc import TOC, toc_to_html from calibre.gui2.tweak_book import current_container from calibre.ebooks.oeb.base import xml2str def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
if match is None:
# All matches found, output the resulting Table of Contents.
# The argument metadata is the metadata of the book being edited if 'toc' in data:
toc = data['toc']
root = TOC()
for (file_name, tag_name, anchor, text) in toc:
parent = root.children[-1] if tag_name == 'h2' and root.children else root parent.add(text, file_name, anchor)
toc = toc_to_html(root, current_container(), 'toc.html', 'Table of Contents for ' + metadata.title, metadata.language)
print(xml2str(toc))
else:
print('No headings to build ToC from found')
else:
# Add an entry corresponding to this match to the Table of Contents if 'toc' not in data:
# The entries are stored in the data object, which will persist
# for all invocations of this function during a 'Replace All' operation data['toc'] = []
tag_name, anchor, text = match.group(1), replace_entities(match.group(2)), replace_entities(match.group(3))
data['toc'].append((file_name, tag_name, anchor, text))
return match.group() # We don't want to make any actual changes, so return the original matched text

# Ensure that we are called once after the last match is found so we can
# output the ToC replace.call_after_last_match = True
# Ensure that when running over multiple files, this function is called,
# the files are processed in the order in which they appear in the book replace.file_order = 'spine'

다음 찾기 표현식과 함께 사용하세요:

<(h[12]) [^<>]* id=['"]([^'"]+)['"][^<>]*>([^<>]+)

:guilabel:`모든 텍스트 파일`에서 검색을 실행하면 검색이 끝날 때 “함수의 디버그 출력” 창이 팝업되며, 여기에 :file:`toc.html`에 붙여넣을 준비가 된 HTML 목차가 포함됩니다.

위 함수는 주석이 많이 달려 있어 따라하기 쉬울 것입니다. 핵심적인 새로운 기능은 replace() 함수의 또 다른 유용한 추가 인수인 data 객체의 사용입니다. data 객체는 단일 모두 바꾸기 작업 동안 ``replace()``의 모든 연속 호출 사이에 지속되는 Python *딕셔너리*입니다.

또 다른 새로운 기능은 call_after_last_match``의 사용입니다. ``replace() 함수에서 이를 ``True``로 설정하면 편집기가 모든 일치 항목을 찾은 후 ``replace()``를 한 번 더 호출합니다. 이 추가 호출에서 match 객체는 ``None``이 됩니다.

이것은 함수 모드의 강력함을 보여주기 위한 데모일 뿐입니다. 실제로 책의 머리글에서 목차를 생성해야 한다면 :guilabel:`도구->목차`의 전용 목차 도구를 사용하는 것이 더 좋습니다.

함수 모드의 API

모든 함수 모드 함수는 다음 서명을 가진 replace라는 이름의 Python 함수여야 합니다:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
return a_string

찾아 바꾸기가 실행되면, 발견된 각 일치 항목에 대해 replace() 함수가 호출되며, 해당 일치 항목에 대한 대체 문자열을 반환해야 합니다. 대체를 수행하지 않아야 하는 경우 원본 문자열인 match.group()``을 반환해야 합니다. ``replace() 함수의 다양한 인수는 아래에 설명되어 있습니다.

match 인수
match 인수는 현재 발견된 일치 항목을 나타냅니다. `Python Match 객체 <https://docs.python.org/library/re.html#match-objects>`_입니다. 가장 유용한 메서드는 ``group()``으로, 검색 정규 표현식의 개별 캡처 그룹에 해당하는 일치 텍스트를 가져오는 데 사용할 수 있습니다.

number 인수
number 인수는 현재 일치 항목의 번호입니다. :guilabel:`모두 바꾸기`를 실행하면 각 연속 일치 항목에 대해 ``replace()``가 증가하는 번호로 호출됩니다. 첫 번째 일치 항목은 번호 1입니다.

file_name 인수

이것은 현재 일치 항목이 발견된 파일의 파일명입니다. 표시된 텍스트 내에서 검색할 때 ``file_name``은 비어 있습니다. ``file_name``은 정규 형식으로, ``/``를 경로 구분자로 사용하여 책의 루트에 상대적인 경로입니다.

metadata 인수

이것은 제목, 저자, 언어 등과 같은 현재 책의 메타데이터를 나타냅니다. calibre.ebooks.metadata.book.base.Metadata 클래스의 객체입니다. 유용한 속성으로는 title, ``authors`(저자 목록), ``language``(언어 코드)가 있습니다.

dictionaries 인수

이것은 현재 책의 철자 검사에 사용되는 사전 모음을 나타냅니다. 가장 유용한 메서드는 ``dictionaries.recognized(word)``로, 전달된 단어가 현재 책의 언어에 대한 사전에서 인식되면 ``True``를 반환합니다.

data 인수

이것은 간단한 Python dictionary``입니다. :guilabel:`모두 바꾸기`를 실행하면 각 연속 일치 항목에 대해 동일한 ``dictionary``가 data로 ``replace()``가 호출됩니다. 따라서 :guilabel:`모두 바꾸기` 작업 동안 ``replace() 호출 사이에 임의의 데이터를 저장하는 데 사용할 수 있습니다.

functions 인수
functions 인수는 다른 모든 사용자 정의 함수에 대한 접근을 제공합니다. 이는 코드 재사용에 유용합니다. 한 곳에서 유틸리티 함수를 정의하고 다른 모든 함수에서 재사용할 수 있습니다. 예를 들어, ``My Function``이라는 함수를 다음과 같이 만든다고 가정합니다:

def utility():
# do something

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
...

그러면 다른 함수에서 utility() 함수에 다음과 같이 접근할 수 있습니다:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
utility = functions['My Function']['utility']
...

함수 객체를 사용하여 영속적인 데이터를 저장하고 다른 함수에서 재사용할 수도 있습니다. 예를 들어, :guilabel:`모두 바꾸기`와 함께 실행될 때 일부 데이터를 수집하는 하나의 함수와 나중에 실행될 때 해당 데이터를 사용하는 another 함수를 만들 수 있습니다. 다음 두 함수를 고려하세요:

# Function One
persistent_data = {}

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
...
persistent_data['something'] = 'some data'

# Function Two
def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
persistent_data = functions['Function One']['persistent_data']
...

함수 디버깅

Python의 표준 print() 함수를 사용하여 만든 함수를 디버그할 수 있습니다. print의 출력은 찾아 바꾸기가 완료된 후 팝업 창에 표시됩니다. 위에서 ``print()``를 사용하여 전체 목차를 출력하는 예시를 보았습니다.

여러 HTML 파일에서 실행할 때 파일 순서 선택

여러 HTML 파일에서 모두 바꾸기`를 실행하면 파일이 처리되는 순서는 편집을 위해 열어둔 파일에 따라 달라집니다. 함수에서 ``file_order` 속성을 설정하여 파일이 나타나는 순서대로 검색이 처리되도록 강제할 수 있습니다:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
...

replace.file_order = 'spine'

file_order``는 ``spine``과 ``spine-reverse 두 값을 받으며, 각각 책에 나타나는 순서대로 또는 역순으로 여러 파일을 처리하도록 합니다.

마지막 일치 항목을 찾은 후 함수를 한 번 더 호출하기

때때로 위의 목차 자동 생성 예시처럼 마지막 일치 항목을 찾은 후 함수를 한 번 더 호출하는 것이 유용합니다. 함수에서 call_after_last_match 속성을 설정하여 이를 수행할 수 있습니다:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
...

replace.call_after_last_match = True

함수의 출력을 표시된 텍스트에 추가

표시된 텍스트에서 찾아 바꾸기를 실행할 때 표시된 텍스트 끝에 일부 텍스트를 추가하는 것이 유용할 수 있습니다. 함수에서 append_final_output_to_marked 속성을 설정하여 이를 수행할 수 있습니다(``call_after_last_match``도 설정해야 합니다):

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
...
return 'some text to append'

replace.call_after_last_match = True
replace.append_final_output_to_marked = True

표시된 텍스트에서 검색 수행 시 결과 대화상자 표시 안 함

함수에서 suppress_result_dialog 속성을 설정하여 결과 대화상자(여러 텍스트 블록에 대한 찾아 바꾸기의 반복 적용을 느리게 할 수 있음)를 표시하지 않도록 할 수도 있습니다:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
...

replace.suppress_result_dialog = True

더 많은 예시
calibre 사용자가 기여한 더 유용한 예시는 `calibre 전자책 편집기 포럼 <https://www.mobileread.com/forums/showthread.php?t=237181>`_에서 확인할 수 있습니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 전자책 편집하는 중 »
- 스니펫

스니펫

캘리버 전자책 편집기는 *스니펫*을 지원합니다. 스니펫은 자주 재사용되거나 많은 중복 텍스트를 포함하는 텍스트 조각입니다. 편집기를 사용하면 몇 번의 키 입력만으로 스니펫을 삽입할 수 있습니다. 예를 들어 HTML 파일을 편집할 때 링크 태그를 자주 삽입한다고 가정해 보겠습니다. 그러면 편집기에서 ``<a``를 입력하고 |ct|를 누르기만 하면 됩니다. 편집기가 이를 다음으로 확장합니다:

<a href="filename"></a>

뿐만 아니라 filename 단어가 선택되고 커서가 그 위에 놓이므로, 편집기의 훌륭한 자동 완성 기능을 사용하여 실제 파일 이름을 쉽게 입력할 수 있습니다. 파일 이름 입력을 마치면 |ct|를 다시 누르면 커서가 <a> 태그 사이의 위치로 이동하여 링크의 텍스트를 쉽게 입력할 수 있습니다.

편집기의 스니펫 시스템은 매우 정교하며, 몇 가지 내장 스니펫이 있고 편집 스타일에 맞게 자신만의 스니펫을 만들 수 있습니다.

아래의 내장 스니펫 설명은 스니펫 시스템의 강력함을 이해하는 데 도움이 될 것입니다.

참고

찾기 및 바꾸기 패널의 텍스트 입력 필드에서도 스니펫을 사용할 수 있습니다. 그러나 자리 표시자(|ct|를 사용하여 이동)는 작동하지 않습니다.

내장된 스니펫

내장 스니펫은 아래에 설명되어 있습니다. 같은 트리거 텍스트로 자신만의 스니펫을 만들어 재정의할 수 있습니다.

채움 텍스트 삽입하기 [Lorem]

첫 번째 내장 스니펫이자 가장 간단한 것은 문서에 채움 텍스트를 삽입하는 데 사용됩니다. 이 채움 텍스트는 키케로의 철학적 저작 `De finibus bonorum et malorum <https://en.wikipedia.org/wiki/De_finibus_bonorum_et_malorum>`_(영어로 번역됨)에서 가져온 것입니다. 사용하려면 HTML 파일에서 ``Lorem``을 입력하고 |ct|를 누르기만 하면 됩니다. 그러면 몇 개의 단락으로 된 채움 텍스트로 대체됩니다.

이 스니펫의 정의는 매우 간단합니다. 트리거 텍스트는 ``Lorem``으로 정의되고 템플릿은 삽입할 리터럴 텍스트로 간단히 정의됩니다. 원하는 형태의 채움 텍스트를 사용하도록 쉽게 사용자 정의할 수 있습니다.

자동 닫힘 HTML 태그 삽입하기 [<>]

이제 *자리 표시자*라는 강력한 개념의 간단한 예를 살펴보겠습니다. 자동 닫힘 태그 ``<hr/>``을 삽입하고 싶다고 가정해 보겠습니다. ``<>``를 입력하고 |ct|를 누르면 편집기가 스니펫을 다음으로 확장합니다:

<|/>

여기서 | 기호는 현재 커서 위치를 나타냅니다. 그런 다음 ``hr``을 입력하고 |ct|를 누르면 커서가 태그 끝 뒤로 이동합니다. 이 스니펫은 다음과 같이 정의됩니다:

Trigger: <>
Template: <$1/>$2

자리 표시자는 단순히 달러($) 기호 뒤에 숫자가 오는 것입니다. |ct|를 눌러 스니펫을 확장하면 커서는 첫 번째 자리 표시자(가장 낮은 숫자의 자리 표시자)에 위치합니다. |ct|를 다시 누르면 커서는 다음 자리 표시자(다음으로 높은 숫자의 자리 표시자)로 이동합니다.

HTML 링크 태그 삽입하기 [<a]

HTML 링크 태그는 모두 공통 구조를 공유합니다. href 속성과 여는 태그와 닫는 태그 사이에 일부 텍스트가 있습니다. 태그 입력을 더 효율적으로 만드는 스니펫은 자리 표시자의 몇 가지 추가 기능을 소개합니다. 이 스니펫을 사용하려면 ``<a``를 입력하고 |ct|를 누르기만 하면 됩니다. 편집기가 이를 다음으로 확장합니다:

<a href="filename|"></a>

뿐만 아니라 filename 단어가 선택되고 커서가 그 위에 놓이므로, 편집기의 훌륭한 자동 완성 기능을 사용하여 실제 파일 이름을 쉽게 입력할 수 있습니다. 파일 이름 입력을 마치면 |ct|를 다시 누르면 커서가 <a> 태그 사이의 위치로 이동하여 링크의 텍스트를 쉽게 입력할 수 있습니다. 텍스트 입력을 마친 후 |ct|를 다시 누르면 닫는 태그 뒤의 지점으로 이동합니다. 이 스니펫은 다음과 같이 정의됩니다:

Trigger: <a
Template: <a href="${1:filename}">${2*}</a>$3

여기에는 몇 가지 새로운 기능이 있습니다. 첫째, $1 자리 표시자가 더 복잡해졌습니다. 이제 *기본 텍스트*(filename 단어)가 포함됩니다. 자리 표시자에 기본 텍스트가 포함되어 있으면 스니펫이 확장될 때 기본 텍스트가 자리 표시자를 대체합니다. 또한 |ct|를 사용하여 기본 텍스트가 있는 자리 표시자로 이동하면 기본 텍스트가 선택됩니다. 이러한 방식으로 기본 텍스트를 사용하여 템플릿의 중요한 부분을 채우라는 알림 역할을 할 수 있습니다. 구문 ``${<숫자>:기본 텍스트}``를 사용하여 자리 표시자의 기본 텍스트를 지정할 수 있습니다.

또 다른 새로운 기능은 두 번째 자리 표시자 뒤에 별표가 있다는 것입니다 (${2*}). 이는 템플릿을 확장하기 전에 선택한 텍스트가 자리 표시자를 대체한다는 것을 의미합니다. 동작을 확인하려면 편집기에서 일부 텍스트를 선택하고 |ct|를 누른 다음 ``<a``를 입력하고 |ct|를 다시 누르면 템플릿이 다음으로 확장됩니다:

<a href="filename">whatever text you selected</a>

HTML 이미지 태그 삽입하기 [<i]

이것은 위에서 본 HTML 링크 삽입과 매우 유사합니다. <img src="filename" alt="description" /> 태그를 빠르게 입력하고 src``와 ``alt 속성 사이를 이동할 수 있습니다:

Trigger: <i
Template: <img src="${1:filename}" alt="${2*:description}" />$3

임의의 HTML 태그 삽입하기 [<<]

이를 통해 임의의 전체 HTML 태그를 삽입할 수 있습니다(또는 이전에 선택한 텍스트를 태그로 감쌀 수 있습니다). 사용하려면 ``<<``를 입력하고 |ct|를 누르기만 하면 됩니다. 선택한 텍스트를 감싸려면 먼저 텍스트를 선택하고 |ct|를 누른 다음 ``<<``를 입력하고 |ct|를 다시 누르세요. 편집기가 이를 다음으로 확장합니다:

<|></>

태그 이름을 입력하세요. 예: ``span``을 입력하고 |ct|를 누르면 다음과 같이 됩니다:

<span>|</span>

닫는 태그가 자동으로 ``span``으로 채워진 것을 알 수 있습니다. 이것은 자리 표시자의 또 다른 기능인 *미러링*으로 구현됩니다. 미러링은 템플릿에서 동일한 자리 표시자를 두 번 이상 지정하면 |ct|를 누를 때 첫 번째 위치에 입력한 내용이 두 번째 및 이후 모든 위치에 자동으로 채워진다는 것을 의미합니다. 이 스니펫의 정의는 다음과 같습니다:

Trigger: <<
Template: <$1>${2*}</$1>$3

볼 수 있듯이 첫 번째 자리 표시자($1)가 두 번 지정되었으며, 두 번째는 닫는 태그 안에 있어 여는 태그에 입력한 내용을 그대로 복사합니다.

class 속성이 있는 임의의 HTML 태그 삽입하기 [<c]

이것은 위의 임의 태그 삽입 예제와 매우 유사하지만, 태그의 class를 지정하려는 것으로 가정합니다:

Trigger: <c
Template: <$1 class="${2:classname}">${3*}</$1>$4

이를 통해 먼저 태그 이름을 입력하고 |ct|를 누른 다음, class 이름을 입력하고 |ct|를 누르고, 태그 내용을 입력한 후 |ct|를 마지막으로 눌러 태그 밖으로 이동할 수 있습니다. 닫는 태그는 자동으로 채워집니다.

나만의 스니펫 만들기

스니펫의 진정한 강점은 편집 스타일에 맞게 자신만의 스니펫을 만들 수 있다는 것입니다. 자신만의 스니펫을 만들려면 편집기에서 편집 → 환경설정 → 편집기 설정 → 스니펫 관리`로 이동하세요. 그러면 자신만의 스니펫을 만드는 데 도움이 되는 사용하기 쉬운 대화상자가 나타납니다. :guilabel:`스니펫 추가 버튼을 클릭하면 다음과 같은 대화상자가 나타납니다:

먼저 스니펫에 나중에 스니펫을 식별하는 데 도움이 되는 설명적인 이름을 지정하세요. 그런 다음 *트리거*를 지정하세요. 트리거는 스니펫을 확장하기 위해 |ct|를 누르기 전에 편집기에 입력해야 하는 텍스트입니다.

그런 다음 스니펫 템플릿을 지정하세요. 위의 예제 중 하나를 시작으로 필요에 맞게 수정하세요. 마지막으로 스니펫이 활성화될 파일 형식을 지정하세요. 이렇게 하면 같은 트리거 텍스트를 가진 여러 스니펫이 서로 다른 파일 형식에서 다르게 작동하게 할 수 있습니다.

다음 단계는 새로 만든 코드 조각을 테스트하는 것입니다. 하단의 테스트 상자를 사용하세요. 트리거 텍스트를 입력하고 Control+J 키를 눌러 코드 조각을 확장하고 자리 표시자 사이를 이동하세요.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- calibre 콘텐츠 서버

calibre 콘텐츠 서버
calibre :guilabel:`콘텐츠 서버`를 사용하면 calibre 라이브러리에 접근하여 좋아하는 모바일 폰이나 태블릿 장치의 브라우저에서 직접 책을 읽을 수 있습니다. 따라서 전용 책 읽기/관리 앱을 폰에 설치할 필요가 없습니다. 브라우저만 사용하면 됩니다. 서버는 읽고 있는 책을 오프라인 캐시에 다운로드하여 저장하므로 인터넷 연결이 없어도 책을 읽을 수 있습니다.

목차

- 다른 장치에서 콘텐츠 서버에 접근하기

- 홈 네트워크의 장치에서 서버에 접근하기

- 인터넷 어디서든 서버에 접근하기

- 서버 인터페이스

- 책 목록

- 책 뷰어

- 브라우저 지원

- 오프라인 지원 활성화

- 명령줄에서만 사용자 계정 관리

- calibre 콘텐츠 서버를 다른 서버에 통합하기

- 전체 가상 호스트 사용

- URL 접두사 사용

- 최신 Linux 시스템에서 calibre 서버용 서비스 만들기

서버를 시작하려면 연결/공유 버튼을 클릭하고 콘텐츠 서버 시작`을 선택하세요. 컴퓨터의 방화벽이나 백신 프로그램에서 ``calibre.exe``에 대한 접근을 허용할지 묻는 메시지가 표시될 수 있습니다. ``허용` 또는 확인 버튼을 클릭하세요. 그런 다음 컴퓨터에서 브라우저(Chrome 또는 Firefox 권장)를 열고 다음 주소를 입력하세요:

http://127.0.0.1:8080

그러면 브라우저에 calibre 라이브러리를 보여주는 페이지가 열립니다. 라이브러리 중 하나를 클릭하여 책을 탐색하세요. 책을 클릭하면 책에 대한 모든 메타데이터와 함께 읽기 및 다운로드 버튼이 표시됩니다. 읽기 버튼을 클릭하여 책 읽기를 시작하세요.

참고

위에서 사용한 주소 ``http://127.0.0.1:8080``은 calibre를 실행 중인 컴퓨터에서만 작동합니다. 다른 컴퓨터/폰/태블릿 등에서 서버에 접근하려면 다음 섹션에서 설명하는 것처럼 약간의 추가 작업이 필요합니다.

다른 장치에서 콘텐츠 서버에 접근하기

일반적으로 필요한 원격 장치 접근에는 두 가지 유형이 있습니다. 첫 번째, 더 간단한 유형은 홈 네트워크 내에서의 접근입니다. 홈 네트워크의 컴퓨터에서 calibre를 실행하고 있고 다른 장치도 동일한 홈 네트워크에 연결되어 있다면 해당 장치에서 서버에 쉽게 접근할 수 있어야 합니다.

홈 네트워크의 장치에서 서버에 접근하기

위에서 설명한 대로 calibre에서 서버를 시작한 후 연결/공유 버튼을 다시 클릭하세요. 콘텐츠 서버 시작 대신 :guilabel:`콘텐츠 서버 중지`가 표시되어야 합니다. 이 동작 오른쪽에 IP 주소와 포트 번호가 나열됩니다. 마침표로 구분된 숫자 형태입니다. 예:

Stop Content server [192.168.1.5, port 8080]

이 숫자들은 장치에서 서버에 연결하는 데 사용할 주소를 알려줍니다. 위 예시를 따르면 주소는 다음과 같습니다:

http://192.168.1.5:8080

주소의 첫 부분은 항상 http://``이고, 다음 부분은 쉼표 이전의 숫자인 IP 주소이며, 마지막으로 콜론(:``)을 사용하여 IP 주소에 추가해야 하는 포트 번호가 있습니다. 운이 좋다면 이것만으로 충분하며 장치에서 calibre 라이브러리를 볼 수 있을 것입니다. 그렇지 않으면 계속 읽어보세요.

홈 네트워크 연결 문제 해결

장치에서 서버에 접근할 수 없는 경우 다음 단계를 시도하세요:

1. 서버와 동일한 컴퓨터에서 실행 중인 브라우저에서 http://127.0.0.1:8080 주소를 열어 서버가 실행 중인지 확인하세요.

2. 방화벽/백신이 포트 ``8080``에서 컴퓨터로의 연결과 calibre 프로그램을 허용하는지 확인하세요. 방화벽/백신이 문제의 원인인지 확인하는 가장 쉬운 방법은 둘 다 일시적으로 끄고 연결을 시도하는 것입니다. 컴퓨터를 안전하게 보호하려면 방화벽을 끄기 전에 먼저 인터넷 연결을 해제하세요.

3. 장치와 컴퓨터가 동일한 네트워크에 있는지 확인하세요. 즉, 둘 다 동일한 무선 라우터에 연결되어 있어야 합니다. 특히 셀룰러나 ISP 제공 직접 WiFi 연결을 사용하고 있으면 안 됩니다.

4. 비표준 네트워크 설정을 사용하고 있다면 연결/공유 메뉴에 표시된 IP 주소가 잘못되었을 수 있습니다. 이러한 경우 사용해야 할 올바른 IP 주소를 직접 파악해야 합니다. 안타깝게도 가능한 네트워크 구성의 무한한 다양성을 감안할 때 이를 위한 로드맵을 제공하기는 어렵습니다.

5. 사용자 이름과 암호를 설정했다면 먼저 그것 없이 시도하여 문제를 일으키는지 확인하세요. 일부 전자잉크 장치는 인증을 처리하지 못하는 브라우저를 가지고 있습니다. 때때로 URL에 사용자 이름과 암호를 포함하여 우회할 수 있습니다. 예: http://username:password@192.168.1.2:8080.

6. 막혔다면 `calibre 사용자 포럼`_에서 도움을 요청할 수 있습니다.

인터넷 어디서든 서버에 접근하기

경고

이 작업을 수행하기 전에 서버에서 사용자 이름/암호 보호를 켜야 합니다. 그렇지 않으면 전 세계 누구나 책에 접근할 수 있게 됩니다. 환경설정 → 공유 → 네트워크를 통한 공유`로 이동하여 :guilabel:`콘텐츠 서버에 접근하려면 사용자 이름과 암호 필요 옵션을 활성화하세요.

인터넷 접근 설정의 구체적인 세부 사항은 사용 중인 네트워크 구성과 컴퓨터 유형에 따라 다르지만, 기본적인 구성은 다음과 같습니다.

1. 서버를 실행할 컴퓨터의 외부 IP 주소를 확인하세요. 컴퓨터에서 실행 중인 브라우저에서 What is my IP address [https://www.whatismyip.com/] 사이트를 방문하여 확인할 수 있습니다.

2. 컴퓨터가 라우터 뒤에 있는 경우, 라우터에서 포트 포워딩을 활성화하여 포트 ``8080``(또는 calibre 콘텐츠 서버를 실행하기 위해 선택한 포트)을 컴퓨터로 전달하세요.

3. 컴퓨터의 방화벽/백신 프로그램에서 calibre 서버가 허용되는지 확인하세요.

4. 이제 첫 번째 단계에서 찾은 IP 주소를 사용하여 인터넷에 연결된 모든 장치에서 서버에 접근할 수 있어야 합니다. 예를 들어, 찾은 IP 주소가 ``123.123.123.123``이고 calibre 서버에 사용하는 포트가 ``8080``이면, 장치에서 사용할 주소는 ``http://123.123.123.123:8080``이 됩니다.

5. 선택적으로, `no-ip <https://www.noip.com/free>`_와 같은 서비스를 사용하여 첫 번째 단계에서 찾은 IP 주소 대신 기억하기 쉬운 주소를 설정할 수 있습니다.

참고

최대한의 보안을 위해 콘텐츠 서버에서 HTTPS도 활성화해야 합니다. 서버의 고급 구성 옵션에서 사용할 HTTPS 인증서의 경로를 직접 제공하거나, 아래 설명된 대로 역방향 프록시를 설정하여 기존 HTTPS 구성을 사용할 수 있습니다.

서버 인터페이스

서버 인터페이스는 터치 스크린 사용에 최적화된 간소화된 버전의 calibre 인터페이스입니다. 홈 화면은 현재 읽고 있는 책을 보여주고, 탐색할 calibre 라이브러리를 선택할 수 있습니다. calibre 서버는 이전과 같이 하나의 라이브러리만이 아니라 모든 라이브러리에 접근할 수 있게 해줍니다.

책 목록

서버 책 목록은 간단한 표지 그리드입니다. 표지를 탭하여 책의 상세 메타데이터를 보거나 책을 읽을 수 있습니다. 더 상세한 목록을 선호하면 오른쪽 상단의 세로 점 세 개를 클릭하여 기본 보기를 변경할 수 있습니다.

책 목록의 정렬 및 검색은 calibre 사용자에게 익숙할 것입니다. 오른쪽 상단 영역의 아이콘을 클릭하여 접근할 수 있습니다. 둘 다 기본 calibre 프로그램과 정확히 동일하게 작동합니다. 검색 페이지에서는 기본 프로그램의 태그 탐색기를 사용하는 것처럼 저자/태그 등을 클릭하여 검색 쿼리를 구성할 수도 있습니다.

기본 프로그램에서 많은 사랑을 받은 기능인 :guilabel:`가상 라이브러리`가 서버 인터페이스에도 있습니다. 오른쪽 상단의 세로 점 세 개를 클릭하여 가상 라이브러리를 선택하세요.

책 뷰어

Calibre 라이브러리에 있는 모든 책은 해당 책을 탭한 다음 ‘읽기’ 버튼을 탭하기만 하면 바로 읽을 수 있습니다. 책 뷰어는 조작이 매우 간단합니다. 탭하거나 스와이프하여 페이지를 넘길 수 있으며, 위아래로 스와이프하면 챕터 간 이동이 가능합니다. 화면 상단 1/4 부분을 탭하면 자세한 설정 및 뷰어 옵션에 접근할 수 있습니다.

콘텐츠 서버를 실행해 두면 여러 장치에서 같은 책을 열어도 마지막 읽은 위치를 기억합니다. 기억하지 못하는 경우 상단 1/4을 탭하고 :guilabel:`동기화`를 선택하여 동기화를 강제할 수 있습니다.

브라우저 지원

새 calibre 서버는 고급 HTML 5 및 CSS 3 기능을 많이 활용합니다. 따라서 최신 브라우저가 필요합니다. Android Chrome, iOS Safari, 그리고 데스크톱의 Chrome과 Firefox에서 테스트되었습니다.

서버는 이미 표준화되었거나 표준화 진행 중인 기능만 사용하도록 주의합니다. 따라서 현재 즐겨 사용하는 브라우저에서 작동하지 않더라도, 해당 브라우저가 업데이트되면 아마 작동할 것입니다.

특히 오래되거나 제한된 브라우저를 사용 중이거나 JavaScript 실행을 원하지 않는 경우, 서버 주소에 ``/mobile``을 추가하여 모바일 보기를 사용할 수 있습니다.

참고
iOS에서 Apple은 단일 브라우저 엔진만 허용하므로 Firefox, Chrome, Safari는 모두 내부적으로 동일한 브라우저입니다. 새 서버 인터페이스는 iOS 10.3.2 이상이 필요합니다. Android에서는 Chrome 버전 58 이상으로 서버가 테스트되었습니다.

오프라인 지원 활성화

서버는 오프라인 모드를 완벽하게 지원합니다. 책을 열고 로딩이 완료되면 오프라인에서 이어서 읽을 수 있습니다. 단, 일반적인 브라우저는 오프라인 모드(서비스 워커)를 활성화하기 위해 HTTPS 연결이 필요합니다. 따라서 위에서 설명한 대로 HTTPS 지원 <cs_https_mode> 설정 을 실행해야 합니다. 홈페이지 하단으로 스크롤하면 오프라인 모드 활성화 상태를 확인할 수 있습니다.

또는 책을 열고 브라우저 탭을 열어둔 상태로 유지할 수도 있습니다. 탭이 열려 있는 한, 인터넷 연결이 끊긴 상태에서도 책을 계속 읽을 수 있습니다.

명령줄에서만 사용자 계정 관리
calibre 프로그램에는 :guilabel:`환경설정`에 서버의 사용자 계정을 관리할 수 있는 좋은 섹션이 있습니다. 그러나 독립 실행형 서버를 실행하고 동일한 컴퓨터/사용자 계정에서 기본 calibre 프로그램을 실행할 수 없는 경우 명령줄만으로도 사용자를 관리할 수 있습니다.

독립 실행형 calibre-server 프로그램의 --manage-users 옵션을 사용하여 사용자 계정을 관리할 수 있습니다. 사용자 데이터베이스를 /srv/calibre 폴더에 저장하려면 다음을 실행하여 만드세요:

calibre-server --userdb /srv/calibre/users.sqlite --manage-users

프롬프트를 따라 사용자 계정을 만들고 권한을 설정하세요. 완료되면 서버를 다음과 같이 실행할 수 있습니다:

calibre-server --userdb /srv/calibre/users.sqlite --enable-auth

이전 단계에서 생성한 사용자 계정을 사용합니다.

calibre 콘텐츠 서버를 다른 서버에 통합하기

여기에서는 calibre 콘텐츠 서버를 다른 서버에 통합하는 방법을 설명합니다. 가장 일반적인 이유는 SSL을 사용하거나 calibre 라이브러리를 더 큰 사이트의 일부로 제공하기 위해서입니다. 기본적인 기법은 calibre 서버를 실행하고 기본 서버에서 역방향 프록시를 설정하는 것입니다.

역방향 프록시는 일반 서버가 들어오는 요청을 수락하고 calibre 서버에 전달하는 방식입니다. 그런 다음 calibre 서버의 응답을 읽고 클라이언트에 전달합니다. 이는 기본 서버와 밀접하게 통합하려고 시도하지 않고 calibre 서버를 그대로 실행할 수 있음을 의미합니다.

전체 가상 호스트 사용

가장 간단한 구성은 calibre 서버에 전체 가상 호스트를 전용으로 사용하는 것입니다. 이 경우 calibre 서버를 다음과 같이 실행하세요:

calibre-server

이제 기본 서버에서 가상 호스트를 설정하세요. 예를 들어 nginx의 경우:

http {
client_max_body_size 64M; # needed to upload large books
}

server {
listen [::]:80;
server_name myserver.example.com;

location / {
proxy_pass http://127.0.0.1:8080;
}
}

또는 Apache의 경우:

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so

<VirtualHost *:80>
ServerName myserver.example.com
AllowEncodedSlashes On
ProxyPreserveHost On
ProxyPass "/" "http://localhost:8080/"
</VirtualHost>

URL 접두사 사용
calibre에 전체 가상 호스트를 전용으로 사용하지 않으려면 URL 접두사를 사용하도록 할 수 있습니다. calibre 서버를 다음과 같이 시작하세요:

calibre-server --url-prefix /calibre --port 8080

여기서 핵심 매개 변수는 --url-prefix /calibre``입니다. 이로 인해 콘텐츠 서버가 ``/calibre 접두사가 붙은 모든 URL을 제공합니다. 이를 확인하려면 브라우저에서 http://localhost:8080/calibre``를 방문하세요. 일반적인 콘텐츠 서버 웹사이트가 표시되지만 이제 ``/calibre 아래에서 실행됩니다.

Nginx를 사용할 때 필요한 설정은 다음과 같습니다:

http {
client_max_body_size 64M; # needed to upload large books
}

proxy_set_header X-Forwarded-For $remote_addr;
location /calibre/ {
proxy_buffering off;
proxy_pass http://127.0.0.1:8080$request_uri;
}
location /calibre {
# we need a trailing slash for the Application Cache to work
rewrite /calibre /calibre/ permanent;
}

Apache의 경우 먼저 :file:`httpd.conf`에 다음을 추가하여 Apache에서 프록시 모듈을 활성화하세요:

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so

프록시 모듈을 활성화하는 정확한 방법은 Apache 설치에 따라 다릅니다. 프록시 모듈을 활성화한 후 :file:`httpd.conf`에 다음 규칙을 추가하세요(또는 가상 호스트를 사용하는 경우 해당 가상 호스트의 conf 파일에 추가하세요):

AllowEncodedSlashes On
RewriteEngine on
RewriteRule ^/calibre/(.*) http://127.0.0.1:8080/calibre/$1 [proxy]
RedirectMatch permanent ^/calibre$ /calibre/

이것으로 충분합니다. 이제 기본 서버에서 /calibre URL 아래에서 calibre 콘텐츠 서버에 접근할 수 있습니다. 위의 규칙은 /calibre 아래의 모든 요청을 포트 8080에서 실행 중인 calibre 서버에 전달하며, 위의 --url-prefix 옵션 덕분에 calibre 서버가 투명하게 처리합니다.

참고

역방향 프록시를 사용할 때 ``–listen-on 127.0.0.1``을 사용하여 calibre 콘텐츠 서버가 localhost에서만 수신하도록 설정해야 합니다. 이렇게 하면 서버가 동일한 컴퓨터, 즉 역방향 프록시에서 오는 연결만 수신합니다.

참고

기본 서버에 SSL을 설정한 경우, calibre 서버에 다이제스트 인증 대신 더 빠른 기본 인증을 사용하도록 설정해야 합니다. 그렇게 하려면 calibre-server``에 ``--auth-mode=basic 옵션을 전달하세요.

최신 Linux 시스템에서 calibre 서버용 서비스 만들기

최신 (systemd [https://www.freedesktop.org/wiki/Software/systemd/]) 기반 Linux 시스템에서 부팅 시 calibre를 실행하는 서비스를 쉽게 만들 수 있습니다. 아래 내용으로 /etc/systemd/system/calibre-server.service 파일을 만들기만 하면 됩니다:

[Unit]
Description=calibre Content server
After=network.target

[Service]
Type=simple
User=mylinuxuser
Group=mylinuxgroup
ExecStart=/opt/calibre/calibre-server "/path/to/calibre library folder"

[Install]
WantedBy=multi-user.target

mylinuxuser``와 ``mylinuxgroup``을 서버를 실행하려는 사용자와 그룹으로 변경하세요. calibre 라이브러리 폴더의 파일을 소유한 동일한 사용자와 그룹이어야 합니다. 서버를 root로 실행하는 것은 일반적으로 좋지 않습니다. 또한 시스템에 맞게 calibre 라이브러리 폴더의 경로를 변경하세요. 필요한 경우 여러 라이브러리를 추가할 수 있습니다. ``calibre-server 명령의 도움말을 참조하세요.

이제 다음을 실행하세요:

sudo systemctl start calibre-server

서버를 시작합니다. 다음으로 상태를 확인하세요:

sudo systemctl status calibre-server

부팅 시 시작되도록 하려면 다음을 실행하세요:

sudo systemctl enable calibre-server

참고
calibre 서버는 실행 중인 X 서버가 필요하지 않지만, 일부 구성 요소가 X 라이브러리에 연결되므로 X 라이브러리가 설치되어 있어야 합니다.

참고
calibre 서버는 systemd 소켓 활성화도 지원하므로 필요한 경우 이를 사용할 수도 있습니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 전자책 비교

전자책 비교

캘리버에는 전자책을 편집하거나 변환한 후 전자책 내부에서 무엇이 변경되었는지 볼 수 있는 통합 전자책 비교 도구가 포함되어 있습니다. EPUB 및 AZW3 형식의 책을 비교할 수 있습니다.

사용하려면 전자책 편집하는 중 도구에서 전자책을 열고 파일 → 다른 책과 비교`를 클릭하거나 :ref:`book_details 패널을 사용하세요. EPUB에서 EPUB로 변환하면 원본 EPUB 파일이 ORIGINAL_EPUB로 저장됩니다. 책 세부정보 패널에서 ORIGINAL_EPUB 항목을 마우스 오른쪽 버튼으로 클릭하고 :guilabel:`EPUB 형식과 비교`를 선택하기만 하면 됩니다.

열리는 비교 도구는 아래 스크린샷과 같습니다. 선택한 책의 텍스트, 스타일 및 이미지의 차이를 보여줍니다.

비교 보기 이해하기

위 스크린샷에서 볼 수 있듯이, 비교 보기는 두 책의 차이점을 나란히 보여줍니다. 차이점과 그 주변의 몇 줄의 컨텍스트만 표시됩니다. 이를 통해 책과 같은 큰 문서 내에서 변경된 내용만 한눈에 쉽게 확인할 수 있습니다.

추가된 텍스트는 녹색 배경으로, 제거된 텍스트는 빨간색 배경으로, 변경된 텍스트는 파란색 배경으로 표시됩니다.

변경된 모든 텍스트의 줄 번호가 양쪽에 표시되어 편집기에서 특정 변경 사항으로 쉽게 이동할 수 있습니다. 편집기에서 비교 도구를 열 때 오른쪽 패널의 줄을 두 번 클릭하여 편집기에서 해당 줄로 자동 이동할 수도 있습니다.

책을 비교할 때 유용한 기술 중 하나는 차이를 계산하기 전에 비교 도구에 텍스트 및 스타일 파일을 정리하도록 지시하는 것입니다. 이렇게 하면 더 깔끔하고 따라하기 쉬운 차이를 얻을 수 있는 경우가 많습니다. 이렇게 하려면 오른쪽 하단의 옵션 버튼을 클릭하고 비교 전 파일 정리`를 선택하세요. 정리는 때때로 원하지 않는 효과를 초래할 수 있는데, 유효하지 않은 마크업을 유효하도록 변경할 수 있기 때문입니다. :guilabel:`옵션 버튼을 통해 차이점 주변에 표시되는 컨텍스트 줄 수를 변경할 수도 있습니다.

하단의 검색 표시줄을 통해 차이점에서 모든 텍스트를 검색할 수 있습니다. 왼쪽 또는 오른쪽 중 어느 패널을 검색할지 지정해야 합니다.

비교 도구 실행하기

비교 도구는 같은 책의 두 버전이 있고 그 사이의 차이를 보고 싶을 때 가장 유용합니다. 이를 위해 도구를 실행하는 여러 가지 방법이 있습니다.

두 전자책 파일 비교하기

전자책 편집하는 중 도구에서 첫 번째 파일을 여세요. 이제 :guilabel:`파일->다른 책과 비교`를 클릭하고 두 번째 파일을 선택하세요(첫 번째 파일과 동일한 형식이어야 합니다). 비교 보기가 열리며 편집 중인 파일이 오른쪽에, 두 번째 파일이 왼쪽에 표시됩니다.

ORIGINAL_FMT와 FMT 비교하기

캘리버에서 FMT에서 자체 형식으로 변환하면 원본 파일이 ORIGINAL_FMT로 저장됩니다. 기본 캘리버 창의 책 상세정보 패널에서 ORIGINAL_FMT 항목을 마우스 오른쪽 버튼으로 클릭하고 :guilabel:`FMT와 비교`를 선택하여 변환으로 변경된 내용을 확인할 수 있습니다. 비교 보기가 열리며 ORIGINAL_FMT가 왼쪽에, FMT가 오른쪽에 표시됩니다.

편집 중 체크포인트와 책의 현재 상태 비교하기

전자책 편집하는 중 도구에는 checkpoints`라는 매우 유용한 기능이 있습니다. 이 기능을 사용하면 책의 현재 상태를 이름이 지정된 *체크포인트*로 저장할 수 있으며, 체크포인트를 만든 후 변경한 내용이 마음에 들지 않으면 해당 체크포인트로 되돌릴 수 있습니다. 편집기에서 다양한 자동 작업을 수행할 때도 체크포인트가 자동으로 생성됩니다. :guilabel:`보기->체크포인트`로 이동하여 체크포인트 목록을 볼 수 있으며, :guilabel:`비교 버튼을 사용하여 선택한 체크포인트의 책을 현재 상태와 비교할 수 있습니다. 비교 도구는 체크포인트를 왼쪽에, 현재 상태를 오른쪽에 표시합니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 전자책 메타데이터 편집

전자책 메타데이터 편집

목차

- 한 번에 한 권의 메타데이터 편집

- 메타데이터 다운로드

- 책 형식 관리

- 표지에 대한 모든 것

- 한 번에 여러 책의 메타데이터 편집

- 찾아 바꾸기

- 메타데이터 일괄 다운로드

- 책에 추가 데이터 파일 추가

전자책은 다양한 형태와 크기로 제공되며 대부분의 경우, 메타데이터(예: 제목/저자/시리즈/발행자)가 불완전하거나 부정확합니다. calibre에서 메타데이터를 변경하는 가장 간단한 방법은 항목을 두 번 클릭하고 올바른 교체 항목을 입력하는 것입니다. 보다 정교한 “파워 편집”을 위해 아래에 설명된 메타데이터 편집 도구를 사용하세요.

한 번에 한 권의 메타데이터 편집

편집하려는 책을 클릭한 다음 메타데이터 편집하기 버튼을 클릭하거나 E 키를 누르세요. 메타데이터의 모든 측면을 편집할 수 있는 대화상자가 열립니다. 보다 빠르고 효율적인 편집을 위한 다양한 기능이 있습니다. 일반적으로 사용되는 팁 목록:

- 제목과 저자 사이의 버튼을 클릭하여 자동으로 바꿀 수 있습니다.

- 저자 정렬명 옆에 있는 버튼을 클릭하면 각 저자와 함께 저장된 정렬 값을 사용하여 calibre가 자동으로 채우도록 할 수 있습니다. 저자 관리 대화상자를 사용하여 저자의 정렬 값을 확인하고 변경하세요. 이 대화상자는 저자 정렬명 옆에 있는 버튼을 길게 클릭하여 열 수 있습니다.

- 태그 옆에 있는 버튼을 클릭하면 책과 관련된 태그를 관리하기 위해 :guilabel:`태그 편집기`를 사용할 수 있습니다.

- “ID” 상자에 ISBN(및 기타 여러 유형의 ID)을 입력할 수 있습니다. 유효하지 않은 ISBN을 입력하면 배경이 빨간색으로 표시됩니다. 유효한 ISBN은 녹색으로 표시됩니다.

- 저자 정렬 값이 calibre가 예상한 값과 다른 경우 저자 정렬 상자가 빨간색으로 표시됩니다.

메타데이터 다운로드

메타데이터 편집 대화상자의 가장 좋은 기능은 다양한 웹사이트에서 메타데이터를 가져와 많은 메타데이터 필드를 자동으로 채우는 기능입니다. 현재 calibre는 Google Books와 Amazon을 사용합니다. 메타데이터 다운로드는 제목, 저자, 시리즈, 태그, 평점, 설명 및 ISBN을 입력할 수 있습니다.

다운로드를 사용하려면 제목과 저자 필드를 입력하고 메타데이터 가져오기 버튼을 클릭하세요. calibre가 제목과 저자에 가장 일치하는 책 목록을 표시합니다. ISBN 필드를 먼저 입력하면 제목과 저자보다 우선하여 사용됩니다. 일치하는 항목이 없으면 제목에 일부 핵심 단어만 포함하고 저자는 성만 입력하여 검색을 덜 구체적으로 만들어 보세요.

책 형식 관리
calibre에서 하나의 책 항목에 여러 가지 다른 *형식*이 연결될 수 있습니다. 예를 들어 셰익스피어 전집을 EPUB 형식으로 얻은 후 Kindle에서 읽기 위해 MOBI로 변환할 수 있습니다. calibre가 여러 형식을 자동으로 관리합니다. 메타데이터 편집 대화상자의 사용 가능한 형식 섹션에서 이러한 형식을 관리할 수 있습니다. 새 형식을 추가하거나 기존 형식을 삭제할 수 있으며, 하나의 형식에 있는 메타데이터에서 책 항목의 메타데이터와 표지를 설정하도록 calibre에 요청할 수도 있습니다.

표지에 대한 모든 것

책의 ISBN이 알려져 있다면 calibre에 책 표지를 다운로드하도록 요청할 수 있습니다. 또는 컴퓨터에서 표지로 사용할 파일을 지정할 수도 있습니다. calibre는 기본 메타데이터가 포함된 기본 표지를 생성할 수도 있습니다. 표지에 이미지를 끌어다 놓아 변경할 수 있으며, 마우스 오른쪽 버튼 클릭으로 표지 이미지를 복사/붙여넣기할 수도 있습니다.

또한, 표지 이미지에 보기 좋지 않은 테두리가 있는 경우 표지에서 테두리를 자동으로 잘라내는 버튼이 있습니다.

한 번에 여러 책의 메타데이터 편집

먼저 Ctrl 또는 Shift`를 누른 상태에서 편집하려는 책을 클릭하여 선택하세요. 두 권 이상의 책을 선택하면 :guilabel:`메타데이터 편집하기 버튼을 클릭하면 일괄 메타데이터 편집 대화상자가 열립니다. 이 대화상을 사용하면 여러 책의 저자/발행자/평점/태그/시리즈 등을 빠르게 동일한 값으로 설정할 수 있습니다. 일부 메타데이터가 공통인 여러 책을 방금 가져온 경우에 특히 유용합니다. 이 대화상자는 매우 강력합니다. 예를 들어, 메타데이터에 대한 일괄 작업을 수행하고 한 열에서 다른 열로 메타데이터를 복사할 수도 있는 찾아 바꾸기 탭이 있습니다.

일반 메타데이터 편집 대화상자에는 여러 책의 메타데이터를 하나씩 편집하는 데 사용할 수 있는 다음 및 이전 버튼도 있습니다.

찾아 바꾸기

여러 책의 메타데이터 편집 대화상자를 사용하면 선택한 책에 대해 임의로 강력한 찾아 바꾸기 작업을 수행할 수 있습니다. 기본적으로 단순 텍스트 찾아 바꾸기를 사용하지만 *정규 표현식*도 지원합니다. 정규 표현식에 대한 자세한 내용은 :ref:`regexptutorial`을 참조하세요.

위에서 언급한 것처럼 두 가지 찾아 바꾸기 모드가 있습니다: 문자 일치와 정규 표현식. 문자 일치는 선택한 검색 필드`에서 `찾을 문자열 상자에 입력한 문자를 찾아 바꿀 문자열 상자에 입력한 내용으로 대체합니다. 필드에서 검색 문자의 각 발생이 대체됩니다. 예를 들어, 검색 중인 필드에 `a bad cat`이 포함되어 있다고 가정합니다. `a`를 `HELLO`로 바꾸도록 검색하면 결과는 `HELLO bHELLOd cHELLOt`이 됩니다.

검색 중인 필드가 태그와 같은 다중 필드이면 각 태그가 개별적으로 처리됩니다. 예를 들어, 태그에 `Horror, Scary`가 포함되어 있으면 검색 표현식 `r,`은 아무것도 일치하지 않습니다. 표현식이 먼저 `Horror`에 적용되고 그 다음 `Scary`에 적용되기 때문입니다.

검색에서 대소문자 차이를 무시하려면 대소문자 구분 확인란의 선택을 해제하세요.

바꾸기 후 함수 적용 상자에서 함수 중 하나를 선택하여 calibre가 결과(바꾸기가 발생한 후의 정보)의 대소문자를 변경하도록 할 수 있습니다. 사용 가능한 작업은 다음과 같습니다:

- 소문자 – 필드의 모든 문자를 소문자로 변경

- 대문자 – 필드의 모든 문자를 대문자로 변경

- 제목 대문자` – 결과의 각 단어를 대문자로 시작하도록 합니다.

사용자 테스트 상자는 찾아 바꾸기가 원하는 대로 수행되는지 확인하기 위해 텍스트를 입력할 수 있는 공간입니다. 대부분의 경우 책 테스트 상자로 충분하지만, 이러한 상자에 표시되지 않는 사례를 확인하고 싶을 수도 있습니다. 해당 사례를 `사용자 테스트`에 입력하세요.

정규 표현식 모드는 (물론) 정규 표현식을 사용하는 것 외에도 문자 모드와 몇 가지 차이점이 있습니다. 첫 번째는 전체 필드가 아니라 검색 문자열과 일치하는 문자열의 일부에 함수가 적용된다는 것입니다. 두 번째는 전체 필드가 아니라 대체 문자열에 함수가 적용된다는 것입니다.

세 번째이자 가장 중요한 것은 대체 문자열이 역참조를 사용하여 검색 문자열의 일부를 참조할 수 있다는 것입니다. 역참조는 ``\n``이며, 여기서 n은 검색 표현식에서 n번째 괄호 그룹을 가리키는 정수입니다. 예를 들어, 위와 동일한 예인 `a bad cat`에서 검색 표현식이 `a (…) (…)`이고 대체 표현식이 `a \2 \1`이면 결과는 `a cat bad`가 됩니다. 역참조에 대한 자세한 내용은 :ref:`regexptutorial`을 참조하세요.

유용한 패턴 하나: 전체 필드의 대소문자를 변경하고 싶다고 가정합니다. 가장 쉬운 방법은 문자 모드를 사용하는 것이지만, 정규 표현식 모드를 사용하고 싶다고 추가로 가정해 보겠습니다. 검색 표현식은 (^.*$), 대체 표현식은 `\1`이어야 하며, 원하는 대소문자 변경 함수를 선택해야 합니다.

마지막으로, 정규 표현식 모드에서는 한 필드에서 다른 필드로 값을 복사할 수 있습니다. 소스 필드와 대상 필드를 다르게 하기만 하면 됩니다. 복사는 대상 필드를 대체하거나, 필드 앞에 추가하거나, 필드 뒤에 추가할 수 있습니다. ‘쉼표 사용’ 확인란은 앞에 추가 및 뒤에 추가 모드에서 텍스트와 대상 필드 사이에 쉼표를 추가할지 여부를 calibre에 알려줍니다. 대상이 다중 필드(예: 태그)인 경우 이 확인란의 선택을 해제할 수 없습니다.

찾아 바꾸기는 다른 탭의 모든 메타데이터 변경 사항이 적용된 후에 수행됩니다. 이로 인해 약간의 혼동이 발생할 수 있습니다. 테스트 상자는 다른 변경 사항 이전의 정보를 표시하지만, 작업은 다른 변경 사항 이후에 적용되기 때문입니다. 발생할 일에 대해 의문이 있으면 찾아 바꾸기를 다른 변경 사항과 혼합하지 마세요.

메타데이터 일괄 다운로드

한 번에 여러 책의 메타데이터를 다운로드하려면 메타데이터 편집하기 버튼을 마우스 오른쪽 버튼 클릭하고 :guilabel:`메타데이터 다운로드`를 선택하세요. 메타데이터만, 표지만, 또는 둘 다 다운로드하도록 선택할 수 있습니다.

책에 추가 데이터 파일 추가
calibre는 책과 관련된 임의의 수의 추가 데이터 파일을 저장할 수 있습니다. 대체 표지, 보충 자료 등이 될 수 있습니다. 직접 보거나 변환 소스로 사용할 수 없습니다. calibre의 전체 텍스트 검색 엔진에 의해 인덱싱되지도 않습니다. 이러한 파일을 보거나 추가하거나 삭제하려면 책을 선택하고 메타데이터 편집하기 버튼을 마우스 오른쪽 버튼 클릭한 다음 데이터 파일 관리`를 선택하세요. 이러한 파일에 대한 작업을 수행할 수 있는 창이 팝업됩니다. 또는 :guilabel:`책 추가 버튼을 마우스 오른쪽 버튼 클릭하고 :guilabel:`선택한 책 레코드에 데이터 파일 추가`를 선택하여 더 빠르게 데이터 파일을 추가할 수도 있습니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 자주 묻는 질문(FAQ)

자주 묻는 질문(FAQ)

목차

- 전자책 형식 변환

- 장치 통합

- 라이브러리 관리

- 기타

전자책 형식 변환

목차

- 캘리버는 어떤 형식으로/에서 변환을 지원합니까?

- 변환에 가장 적합한 원본 형식은 무엇인가요?

- PDF 파일을 변환했는데 결과물에 여러 문제가 있습니다?

- 영문이 아닌 문자가 들어 있거나 스마트 인용부호가 포함된 파일은 어떻게 변환하나요?

- MOBI 파일의 목차는 왜 그런가요?

- 특정 순서로 된 HTML 파일 모음을 어떻게 변환하나요?

- calibre로 만든 EPUB이 유효하지 않습니다?

- 변환 도구의 고급 기능 일부는 어떻게 사용하나요?

캘리버는 어떤 형식으로/에서 변환을 지원합니까?

calibre는 다양한 입력 형식을 다양한 출력 형식으로 변환할 수 있습니다. 아래 목록에 있는 모든 입력 형식을 모든 출력 형식으로 변환할 수 있습니다.

입력 형식: AZW, AZW3, AZW4, CBZ, CBR, CB7, CBC, CHM, DJVU, DOCX, EPUB, FB2, FBZ, HTML, HTMLZ, KEPUB, LIT, LRF, MOBI, ODT, PDF, PRC, PDB, PML, RB, RTF, SNB, TCR, TXT, TXTZ

출력 형식: AZW3, EPUB, DOCX, FB2, HTMLZ, KEPUB, OEB, LIT, LRF, MOBI, PDB, PMLZ, RB, PDF, RTF, SNB, TCR, TXT, TXTZ, ZIP

참고

PRC는 범용 형식이며, calibre는 TextRead 및 MOBIBook 헤더가 있는 PRC 파일을 지원합니다. PDB도 범용 형식입니다. calibre는 eReader, Plucker(입력 전용), PML 및 zTxt PDB 파일을 지원합니다. DJVU 지원은 포함된 텍스트가 있는 DJVU 파일을 변환하는 경우에만 해당합니다. 이런 파일은 보통 OCR 소프트웨어가 생성합니다. MOBI 책은 Mobi6와 KF8의 두 가지 유형이 있을 수 있으며 calibre는 둘 다 완전히 지원합니다. MOBI 파일은 종종 .azw 또는 .azw3 확장자를 사용합니다. Microsoft Word 2007 이상에서 만든 DOCX 파일도 지원합니다.

변환에 가장 적합한 원본 형식은 무엇인가요?

선호도 높은 순서: LIT, MOBI, AZW, EPUB, KEPUB, AZW3, FB2, FBZ, DOCX, HTML, PRC, ODT, RTF, PDB, TXT, PDF

PDF 파일을 변환했는데 결과물에 여러 문제가 있습니다?

PDF는 변환 원본으로 쓰기에 매우 좋지 않은 형식입니다. PDF를 변환할 때 겪게 되는 여러 문제 목록은 PDF 문서 변환 를 참고하세요.

영문이 아닌 문자가 들어 있거나 스마트 인용부호가 포함된 파일은 어떻게 변환하나요?

이 문제에는 두 가지 측면이 있습니다:

1. 원본 파일의 인코딩 파악: calibre는 원본 파일이 어떤 문자 인코딩을 사용하는지 추측하려고 하지만, 많은 경우 이것은 불가능하므로 어떤 인코딩을 사용할지 직접 알려줘야 합니다. GUI에서는 변환 대화상자의 모양새 → 텍스트 섹션에 있는 입력 문자 인코딩 필드에서 설정할 수 있습니다. 명령줄 도구에는 ebook-convert-txt-input --input-encoding 옵션이 있습니다.

2. HTML 파일을 calibre에 추가할 때도 파일이 어떤 인코딩인지 calibre에 알려줘야 할 수 있습니다. 이를 위해 환경설정 → 고급 → 플러그인 → 파일 형식 으로 가서 HTML to ZIP 플러그인을 사용자 지정한 뒤 HTML 파일의 인코딩을 지정하세요. 그러면 calibre에 HTML 파일을 추가할 때 올바르게 처리됩니다. 서로 다른 출처의 HTML 파일은 인코딩이 자주 다르므로 이 설정을 반복해서 바꿔야 할 수도 있습니다. 웹의 많은 파일에서 흔한 인코딩은 cp1252 이며, 먼저 이것을 시도해 보는 것을 권합니다. HTML 파일을 변환할 때는 위에서 언급한 입력 인코딩 설정을 비워 두세요. 이는 HTML to ZIP 플러그인이 HTML 파일을 표준 인코딩(UTF-8)으로 자동 변환하기 때문입니다.

MOBI 파일의 목차는 왜 그런가요?

먼저 이해해야 할 점은 대부분의 전자책에는 목차가 두 개 있다는 것입니다. 하나는 종이책에서 보는 것과 같은 전통적인 목차입니다. 이 목차는 문서 본문의 흐름 일부이며 원하는 대로 스타일을 지정할 수 있습니다. 이 목차를 본문 목차*(*content ToC)라고 합니다.

그리고 메타데이터 목차*(*metadata ToC)가 있습니다. 메타데이터 목차는 책 본문 일부가 아니며, 보통 리더기의 특별한 버튼으로 접근합니다. 예를 들어 calibre 전자책 뷰어에서는 목차 표시 버튼으로 이 목차를 볼 수 있습니다. 이 목차는 책 제작자가 스타일을 지정할 수 없으며, 어떻게 표시될지는 뷰어 프로그램에 달려 있습니다.

MOBI 형식에서는 상황이 조금 혼란스럽습니다. 주류 전자책 형식 가운데 MOBI만 메타데이터 목차*를 제대로 지원하지 않기 때문입니다. MOBI 책은 책 끝에 *추가 본문 목차를 넣어서 메타데이터 목차가 있는 것처럼 흉내 냅니다. Kindle에서 목차로 이동을 누르면 Kindle은 이 추가 본문 목차로 이동합니다.

그래서 MOBI 책에 동일한 목차가 두 개 있는 것처럼 보일 수 있습니다. 하나는 의미상 본문 목차이고 다른 하나는 메타데이터 목차라는 점을 기억하세요. 두 목차의 항목이 완전히 같고 모양도 같을 수 있지만 의미는 다릅니다. 하나는 Kindle 메뉴에서 직접 접근할 수 있고, 다른 하나는 그렇지 않습니다.

MOBI로 변환할 때 calibre는 입력 문서에서 메타데이터 목차*를 감지하고 출력 MOBI 파일 끝에 목차를 생성합니다. 이 동작은 MOBI 출력 설정의 옵션으로 끌 수 있습니다. MOBI 출력 설정의 옵션으로 이 목차를 책의 시작이나 끝에 둘지도 지정할 수 있습니다. 이 목차는 의미상 *메타데이터 목차*이며, MOBI 이외의 형식에서는 *텍스트의 일부가 될 수 없습니다. MOBI에서 텍스트 일부처럼 보이는 것은 MOBI의 한계 때문에 생긴 우연입니다. 문서 본문에서 특정 위치에 목차를 두고 싶다면 직접 만드세요. 따라서 기본값, 즉 메타데이터 목차를 책 끝에 두는 설정을 그대로 유지하는 것을 강력히 권장합니다. 또한 파일 끝 목차 생성을 끄면 결과 MOBI 파일이 Kindle에서 올바르게 동작하지 않을 수 있습니다. Kindle은 Page Flip 기능을 포함해 여러 기능에 메타데이터 목차를 사용하기 때문입니다.

입력 문서에 직접 편집한 목차가 있다면 calibre의 목차 감지 옵션을 사용해 거기서 메타데이터 목차를 자동 생성할 수 있습니다. 이 옵션 사용법의 자세한 내용은 사용자 설명서의 변환 섹션을 참고하세요.

마지막으로, 전자책에서는 본문 목차를 없애고 메타데이터 목차만 두는 것을 권합니다. 메타데이터 목차는 전자책을 읽는 사람에게 훨씬 더 좋은 탐색 경험을 제공합니다(Kindle에서는 사실상 본문 목차와 거의 같다는 점만 예외입니다).

참고

더 새로운 AZW3 형식은 메타데이터 목차를 제대로 지원합니다. 하지만 Kindle 펌웨어는 파일 끝에 생성되는 인라인 목차 생성을 끄면 오동작하는 경향이 있습니다. 따라서 생성된 목차는 그대로 두는 것이 좋습니다. 메타데이터 목차는 있으나 파일 끝에 생성된 목차가 없는 AZW3 파일을 만들면 Kindle의 일부 기능(예: Page Flip)이 동작하지 않습니다.

특정 순서로 된 HTML 파일 모음을 어떻게 변환하나요?

특정 순서로 HTML 파일 모음을 변환하려면 목차 파일을 만들어야 합니다. 즉, 원하는 순서대로 다른 모든 파일에 대한 링크가 들어 있는 또 하나의 HTML 파일이 필요합니다. 예를 들면 다음과 같습니다:

<html>
<body>
<h1>Table of Contents</h1>
<p style="text-indent:0pt">
<a href="file1.html">First File</a><br/>
<a href="file2.html">Second File</a><br/>
.
.
.
</p>
</body>
</html>

그런 다음 이 HTML 파일을 GUI에 추가하고 변환 버튼으로 전자책을 만드세요. 변환 대화상자의 목차 섹션에 있는 옵션을 사용해 목차 생성 방식을 제어할 수 있습니다.

참고

기본적으로 HTML 파일을 추가할 때 calibre는 파일 안의 링크를 깊이 우선 순서로 따라갑니다. 즉 A.html이 B.html, C.html, D.html에 링크하고 B.html도 D.html에 링크한다면 파일 순서는 A.html, B.html, D.html, C.html이 됩니다. 대신 A.html, B.html, C.html, D.html 순서를 원한다면 calibre가 파일을 너비 우선 순서로 추가하도록 해야 합니다. 환경설정 → 고급 → 플러그인 → 파일 형식 으로 가서 HTML to ZIP 플러그인을 사용자 지정하세요.

calibre로 만든 EPUB이 유효하지 않습니다?

calibre는 자신이 만든 EPUB이 유효하다고 보장하지 않습니다. calibre가 보장하는 유일한 것은 유효한 XHTML 1.1 + CSS 2.1을 입력하면 유효한 EPUB을 출력한다는 점입니다. calibre는 자신이 만든 EPUB이 매우 다양한 장치에서 실제로 의도대로 동작하도록 최선을 다하며, 이 목표는 유효한 EPUB만 만드는 것과 양립하지 않을 수 있지만 대다수 사용자에게는 그쪽이 훨씬 더 중요합니다. 항상 유효한 EPUB만 만들어야 하는 도구가 필요하다면 calibre는 맞지 않습니다. 즉 calibre가 만든 EPUB을 EPUB 유효성 검사기를 사용하는 온라인 서점에 보내려면 EPUB의 유효성은 직접 확인해야 합니다. calibre가 대신 해주지 않습니다. 다시 말해 입력 문서로 유효한 XHTML + CSS를 제공해야 합니다.

변환 도구의 고급 기능 일부는 어떻게 사용하나요?

변환기의 각 개별 기능 도움말은 GUI에서 그 위에 마우스를 올리거나 터미널에서 ebook-convert dummy.html .epub -h 를 실행하면 볼 수 있습니다. 시작하기 좋은 자료로는 고급 기능 일부를 보여 주는 다음 데모 파일이 있습니다: html-demo.zip [https://calibre-ebook.com/downloads/html-demo.zip].

장치 통합

목차

- calibre는 어떤 장치를 지원하나요?

- 내 장치가 calibre에서 지원되도록 하려면 어떻게 해야 하나요?

- 내 장치가 calibre에서 감지되지 않습니다?

- 내 장치가 표준이 아니거나 특이합니다. 연결하려면 어떻게 하나요?

- iPad/iPhone/iPod touch에서 calibre를 사용하려면 어떻게 하나요?

- Android 휴대폰/태블릿이나 Kindle Fire에서 calibre를 사용하려면 어떻게 하나요?

- Kindle이나 다른 읽기 장치의 웹 브라우저에서 calibre 책에 접근할 수 있나요?

- calibre로 이메일을 보낼 수 없습니다?

- 내 장치가 Linux에 읽기 전용으로 마운트되고 있으므로 캘리버가 장치에 연결할 수 없습니까?

- 왜 calibre는 Kindle의 컬렉션이나 Nook의 선반을 지원하지 않나요?

- 내 Kobo Touch/Glo/등에서 캘리버를 사용하려고 하면 오류가 발생합니까?

- 내가 e-ink Kindle로 보낸 책의 표지가 잠깐 나타났다가 일반 표지로 바뀝니다?

- Kindle ColorSoft 이상으로 보낸 책의 표지가 책 목록에 나타나지 않습니다?

- 내 MOBI 파일 표지가 Kindle for PC/Kindle for Android/iPad 등에서 더 이상 보이지 않습니다.

- calibre로 Kindle에 책 몇 권을 전송했는데 나타나지 않습니다?

calibre는 어떤 장치를 지원하나요?

calibre는 주요 전자책 리더기 대부분(그리고 소수 장치의 상당수), 스마트폰, 태블릿 등에 직접 연결할 수 있습니다. 또한 폴더에 연결 기능을 사용하면 USB 디스크로 인식되는 모든 전자책 리더와 함께 사용할 수 있습니다. 마지막으로 웹 브라우저가 있는 모든 장치에는 calibre 콘텐츠 서버를 통해 무선으로 연결할 수 있습니다.

내 장치가 calibre에서 지원되도록 하려면 어떻게 해야 하나요?

장치가 운영체제에서 USB 디스크로 보인다면 calibre에 대한 지원을 추가하는 일은 매우 쉽습니다. 다음 정보만 보내주시면 됩니다:

- 장치가 지원하는 전자책 형식의 전체 목록.

- 모든 전자책 파일을 넣어야 하는 특별한 폴더가 장치에 있나요? 또한 하위 폴더에 넣은 파일도 장치가 감지하나요?

- 또한 calibre가 자동 수집하는 장치 정보도 필요합니다. 먼저 장치가 SD 카드를 지원한다면 SD 카드를 삽입하세요. 그다음 장치를 컴퓨터에 연결합니다. calibre에서 환경설정 → 기타 로 가서 “장치 감지 디버그” 버튼을 누르세요. 그러면 디버그 출력이 생성됩니다. 이 출력을 파일에 복사한 뒤, 이번에는 장치를 컴퓨터에서 분리한 상태로 같은 과정을 반복하세요.

- 위의 두 출력과 다른 정보를 함께 보내주시면 해당 장치용 드라이버를 작성하겠습니다.

특정 운영체제용 출력을 보내주시면 해당 운영체제에서의 장치 지원이 다음 calibre 릴리스에 들어갑니다. 출력을 보내려면 버그 보고를 열고 출력을 첨부하세요. 버그 보고 방법 [https://calibre-ebook.com/ko/bugs] 을 참고하세요.

내 장치가 calibre에서 감지되지 않습니다?

문제를 찾으려면 다음 단계를 따르세요:

- 한 번에 하나의 장치만 컴퓨터에 연결되어 있는지 확인하세요. iPhone/iPad 같은 다른 calibre 지원 장치를 동시에 연결하지 마세요.

- Apple iDevice(iPad, iPod Touch, iPhone)를 연결하는 경우, Apple은 더 이상 USB 케이블을 통해 서드파티 소프트웨어가 장치에 연결하는 것을 허용하지 않습니다. 대신 calibre 콘텐츠 서버를 통한 무선 연결을 사용하세요.

- 2024년 Kindle 이후 모델이나 Android 장치를 연결 중이고 macOS 또는 Linux를 사용 중이라면 USB 케이블 사용 아래의 참고를 읽으세요.

- macOS에서 장치를 calibre에 연결할 때 권한 오류가 난다면 시스템 환경설정 > 보안 및 개인정보 보호 > 개인정보 보호 > 파일 및 폴더 에서 해결할 수 있습니다.

- 최신 버전의 calibre(현재 9.7.0)를 실행 중인지 확인하세요. 최신 버전은 언제나 calibre 웹사이트 [https://calibre-ebook.com/ko/download] 에서 받을 수 있습니다. 현재 실행 중인 calibre 버전은 calibre 기본 창 맨 아래 줄에서 확인할 수 있습니다.

- 운영체제가 장치를 인식하고 있는지 확인하세요. 즉 장치가 Windows에서는 탐색기, macOS에서는 Finder에 보여야 합니다.

- calibre에서 환경설정 → 무시된 장치 로 가서 장치가 무시 목록에 들어 있지 않은지 확인하세요.

- 위의 단계를 모두 해도 실패한다면 장치를 연결한 상태에서 환경설정 → 기타 로 가서 장치 감지 디버그 를 누르고, 그 출력을 calibre 버그 추적기 [https://bugs.launchpad.net/calibre] 에 티켓으로 올리세요.

내 장치가 표준이 아니거나 특이합니다. 연결하려면 어떻게 하나요?

연결/공유 버튼 아래에 있는 폴더에 연결 기능 외에도 calibre에는 운영체제에서 디스크 드라이브로 보이는 모든 USB 장치에 연결할 수 있는 사용자 정의 장치 플러그인이 있습니다. 참고: Windows에서는 calibre가 장치를 사용하려면 드라이브 문자가 있어야 합니다. 자세한 내용은 장치 플러그인 환경설정 -> 플러그인 -> 장치 플러그인 -> 사용자 정의 와 환경설정 -> 기타 -> 사용자 정의 장치 설정 정보 가져오기 를 보세요. 또한 보통 내장 calibre 플러그인이 감지하는 장치에 사용자 정의 플러그인을 사용 중이라면, 사용자 정의 플러그인이 대신 사용되도록 내장 플러그인을 먼저 비활성화해야 합니다.

iPad/iPhone/iPod touch에서 calibre를 사용하려면 어떻게 하나요?

Apple 장치에서 calibre 컬렉션을 쉽게 둘러보는 방법은 calibre 콘텐츠 서버 를 사용하는 것입니다. 이렇게 하면 컬렉션을 인터넷을 통해 사용할 수 있습니다. 먼저 calibre에서 다음 단계를 수행하세요.

- calibre의 선호 출력 형식을 EPUB로 설정합니다(출력 형식은 환경설정 → 인터페이스 → 동작 에서 설정할 수 있습니다).

- 출력 프로필을 iPad로 설정합니다(iPhone/iPod에서도 동작합니다). 위치는 환경설정 → 변환 → 공통 옵션 → 페이지 설정 입니다.

- 읽고 싶은 책을 선택하고 변환 버튼을 눌러 EPUB 형식으로 변환합니다.

- 연결/공유 버튼을 눌러 콘텐츠 서버를 켜고 calibre를 실행한 상태로 두세요. 환경설정 → 공유 → 인터넷을 통한 공유 에서 콘텐츠 서버를 자동 시작하도록 설정할 수도 있습니다.

콘텐츠 서버를 사용하면 Safari 자체에서 직접 책을 읽을 수 있습니다. 또한 calibre 콘텐츠 서버에 연결할 수 있는 iDevice용 앱도 많이 있습니다. 예: Marvin, Mapleread, iBooks.

컨텐츠 서버 사용

Safari를 열고 calibre 서버를 실행 중인 컴퓨터의 IP 주소와 포트를 다음과 같이 입력하세요:

http://192.168.1.2:8080/

192.168.1.2 는 calibre를 실행 중인 컴퓨터의 로컬 IP 주소로 바꾸세요. 서버 실행 방법과 올바른 IP 주소 확인 방법은 calibre 콘텐츠 서버 를 참고하세요.

Safari에 책 목록이 표시됩니다. 원하는 책을 탭하면 다운로드할지, 브라우저에서 바로 읽을지 선택할 수 있습니다. 다운로드를 선택하면 Safari가 iBooks로 열지 물어봅니다.

많은 읽기 앱이 calibre 라이브러리를 직접 탐색하는 OPDS [https://opds.io/] 를 지원합니다. 이런 앱에서는 온라인 카탈로그 화면에서 calibre 서버의 IP 주소를 추가해 앱 안에서 calibre 라이브러리의 책을 찾아보고 다운로드할 수 있습니다.

Android 휴대폰/태블릿이나 Kindle Fire에서 calibre를 사용하려면 어떻게 하나요?

Android 장치를 calibre에 연결하는 방법은 두 가지입니다. USB 케이블을 사용하는 방법과 무선으로 연결하는 방법입니다. Android 장치를 사용하기 위한 첫 단계는 전자책 읽기 앱을 장치에 설치하는 것입니다. Android용 전자책 읽기 앱은 무료와 유료 모두 많습니다. 예시(특정 순서 없음): FBReader [https://play.google.com/store/apps/details?id=org.geometerplus.zlibrary.ui.android&hl=en], Moon+ [https://play.google.com/store/apps/details?id=com.flyersoft.moonreader&hl=en], Mantano [https://play.google.com/store/apps/details?id=com.mantano.reader.android.lite&hl=en], Aldiko [https://play.google.com/store/apps/details?id=com.aldiko.android&hl=en], Kindle [https://play.google.com/store/apps/details?id=com.amazon.kindle&feature=related_apps].

USB 케이블 사용

장치를 USB 케이블로 컴퓨터에 연결하기만 하면 됩니다. calibre가 자동으로 장치를 감지해야 하며, 그다음 장치로 보내기 버튼으로 책을 전송할 수 있습니다. macOS와 Linux에서는 한 번에 하나의 프로그램만 Android 장치에 연결할 수 있으므로, OS 파일 관리자나 Android File Transfer 유틸리티 등에서 장치를 열어 두지 않았는지 확인하세요.

참고

최신 Android 장치에서는 연결이 동작하게 하려면 몇 단계를 더 거쳐야 할 수 있습니다. Google이 사용자가 자사 클라우드에 의존하지 않는 것을 정말 원하지 않기 때문입니다. 먼저 USB 케이블을 꽂기 전에 화면 잠금을 해제하세요. USB 케이블을 꽂으면 팝업 알림이 나타납니다. 거기에 “미디어 파일 전송” 또는 “MTP(미디어 전송 모드)” 같은 문구가 표시되는지 확인하세요. 아니라면 알림을 눌러 모드를 미디어 전송(MTP)으로 바꾸세요. 이 시점에서 장치를 인식시키기 위해 calibre를 다시 시작해야 할 수도 있습니다. 마지막으로 calibre나 운영체제가 실제로 장치에 연결을 시도할 때마다 장치에 권한을 묻는 팝업이 뜰 수 있으니 확인을 누르세요.

참고

Kindle Fire 8 이상에서는 USB 케이블을 꽂으면 장치가 충전 중이라는 아이콘이 표시됩니다. 그것을 눌러 장치를 데이터 전송 모드로 전환한 다음 calibre를 시작하면 감지될 것입니다.

무선 연결
calibre에는 내장 웹 서버인 calibre 콘텐츠 서버 가 있습니다. 이 서버는 calibre 컬렉션을 인터넷을 통해 사용할 수 있게 해줍니다. 장치에서는 간단한 브라우저나 전용 앱으로 둘러볼 수 있습니다. 먼저 calibre에서 다음 단계를 수행하세요:

- 일반 Android 장치의 경우 선호 출력 형식 을 EPUB로, Kindle의 경우 MOBI로 설정합니다(출력 형식은 환경설정 → 인터페이스 → 동작 에서 설정할 수 있습니다).

- 장치에서 읽고 싶은 책을 선택하고 변환 버튼을 눌러 EPUB/MOBI 형식으로 변환합니다.

- calibre 환경설정에서 콘텐츠 서버 를 켜고 calibre를 실행한 상태로 둡니다.

이제 Android 장치에서 브라우저를 열고 다음 주소로 이동하세요.

http://192.168.1.2:8080/

192.168.1.2 는 calibre를 실행 중인 컴퓨터의 로컬 IP 주소로 바꾸세요. 서버 실행 방법과 올바른 IP 주소 확인 방법은 calibre 콘텐츠 서버 를 참고하세요.

이제 calibre에서 장치로 책 컬렉션을 둘러보고, Android 장치에 있는 전자책 읽기 소프트웨어로 열 수 있도록 책을 다운로드할 수 있습니다.

많은 읽기 앱이 calibre 라이브러리를 직접 탐색하는 OPDS [https://opds.io/] 를 지원합니다. 이런 앱에서는 온라인 카탈로그 화면에서 calibre 서버의 IP 주소를 추가해 앱 안에서 calibre 라이브러리의 책을 찾아보고 다운로드할 수 있습니다.

Kindle이나 다른 읽기 장치의 웹 브라우저에서 calibre 책에 접근할 수 있나요?

calibre에는 calibre의 책을 웹 페이지로 내보내는 콘텐츠 서버 가 있습니다. 자세한 내용은 calibre 콘텐츠 서버 를 참고하세요.

Kindle(1/2/DX) 같은 일부 장치는 콘텐츠 서버 기본 포트인 8080에 접근할 수 없습니다. 이 경우 calibre 환경설정에서 포트를 80으로 바꾸세요. (일부 운영체제에서는 보안 설정 때문에 1024보다 작은 포트 번호에서 서버를 실행하지 못할 수 있습니다. 이 경우 가장 쉬운 해결책은 라우터에서 포트 80 요청을 포트 8080으로 전달하도록 설정하는 것입니다.)

또한 일부 장치는 콘텐츠 서버가 사용하는 앱 형태의 인터페이스를 실행할 만큼 브라우저가 충분히 고급스럽지 않습니다. 이런 장치에서는 서버 URL 뒤에 /mobile 을 추가해 간단한 비자바스크립트 인터페이스를 사용할 수 있습니다.

calibre로 이메일을 보낼 수 없습니다?

이메일에는 스팸이 많기 때문에 메일 전송은 까다로울 수 있습니다. 메일 서버마다 메일 차단 전략이 다르기 때문입니다. 가장 흔한 문제는 calibre에서 메일 릴레이 없이 직접 이메일을 보내는 경우입니다. 많은 서버(예: Amazon)는 잘 알려진 릴레이를 거치지 않은 이메일을 차단합니다. calibre에서 이메일 전송을 가장 안정적으로 설정하는 방법은 다음과 같습니다:

- GMX [https://www.gmx.com] 에서 무료 GMX 계정을 만드세요.

- calibre에서 환경설정 → 공유 → 이메일로 책 공유 로 가서 GMX 사용 버튼을 누른 뒤 요청되는 정보를 입력하세요.

- 웹사이트에서 GMX 계정에 로그인하고 SMTP 전송을 활성화하세요(Settings->POP3 & IMAP->Send and receive emails via external program).

- 그러면 calibre가 GMX를 사용해 메일을 보낼 수 있게 됩니다.

- Kindle로 보내는 경우 Amazon Kindle 페이지의 이메일 환경설정에서 GMX 이메일 주소에서 보낸 메일을 허용하도록 반드시 업데이트하세요. 또한 Amazon은 AZW3와 새 스타일(KF8) MOBI 파일의 이메일 전송을 허용하지 않습니다. 마지막으로 Amazon은 최근 실제로 책을 전달하기 전에 GMX 계정으로 온 확인 이메일의 링크를 눌러야 하는 방식을 도입했습니다.

이렇게 한 뒤에도 문제가 있을 수 있습니다. 흔한 원인 중 하나는 잘못 설계된 일부 백신 프로그램이 calibre의 이메일 전송 연결을 막는 것입니다. 백신 프로그램에서 calibre를 예외 목록에 추가해 보세요.

참고

Microsoft/GMX는 대량의 이메일 발송에 계정을 사용하면 계정을 비활성화할 수 있습니다. 그래서 calibre는 이런 서비스를 사용할 때 자동으로 5분마다 한 권만 보내도록 제한합니다. 계정 차단 위험을 감수하더라도 이 대기 시간을 줄이고 싶다면 calibre의 환경설정 → 고급 → 트윅 으로 가세요.

참고

Google은 최근 더 많은 광고를 보여 주기 위해 모두가 자사 웹 인터페이스를 쓰도록 유도하려는 의도로 이메일 전송 프로토콜(SMTP) 지원을 의도적으로 망가뜨렸습니다. 그들은 SMTP가 안전하지 않다고 주장하지만, 그것은 사실이 아니며 핑계일 뿐입니다. 대신 다른 이메일 제공업체를 사용하세요.

참고
calibre에 이메일 계정 접근 권한을 주는 것이 걱정된다면, GMX나 Outlook에서 새 무료 이메일 계정을 만들어 calibre 전용으로만 사용하면 됩니다.

내 장치가 Linux에 읽기 전용으로 마운트되고 있으므로 캘리버가 장치에 연결할 수 없습니까?

Linux 커널은 파일시스템에 오류가 있으면 장치를 읽기 전용으로 마운트합니다. 다음 명령으로 파일시스템을 복구할 수 있습니다:

sudo fsck.vfat -y /dev/sdc

/dev/sdc는 장치의 장치 노드 경로로 바꾸세요. 장치 노드는 항상 /dev 아래에 있으며, 다음 명령의 출력에서 확인할 수 있습니다:

mount

왜 calibre는 Kindle의 컬렉션이나 Nook의 선반을 지원하지 않나요?

Kindle과 Nook 모두 USB 연결을 통해 컬렉션을 조작할 방법을 제공하지 않습니다. 컬렉션 사용이 정말 중요하다면 Kindle/Nook를 팔고 Kobo를 사는 것을 권합니다. Kobo만이 전자잉크 화면에서 컬렉션을 하나씩 입력하기에는 인생이 너무 짧다는 점을 이해하는 것 같습니다 😇

Kindle의 경우 USB를 통해 컬렉션을 조작하는 방법이 있기는 하지만, 컬렉션 변경 사항을 인식시키려면 컴퓨터에서 분리할 때마다 Kindle을 매번 재부팅해야 합니다. 그래서 calibre 개발자가 이것을 지원할 만큼 동기를 느낄 가능성은 낮습니다. 다만 calibre 메타데이터로 Kindle 컬렉션을 만들 수 있는 calibre 플러그인은 있습니다. 여기 [https://www.mobileread.com/forums/showthread.php?t=244202] 에서 구할 수 있습니다.

참고

Amazon은 Kindle Touch와 Kindle Fire 같은 최신 모델에서 컬렉션을 조작하는 기능을 완전히 제거했습니다. 따라서 Kindle을 루팅하고 커스텀 펌웨어를 설치하지 않는 한 위 플러그인도 쓸모가 없습니다.

내 Kobo Touch/Glo/등에서 캘리버를 사용하려고 하면 오류가 발생합니까?

Kobo에는 버그가 많은 펌웨어가 있습니다. 연결이 무작위로 실패하는 것으로 알려져 있습니다. 마더보드, USB 포트/케이블/허브의 특정 조합은 이러한 실패 경향을 악화시킬 수 있습니다. 캘리버로 사용하여 터치에 연결할 때 오류가 발생하는 경우 다음을 시도해 보십시오. 각 항목은 일부 캘리버 사용자의 문제를 해결했습니다.

- Kobo를 USB 허브가 아니라 컴퓨터에 직접 연결하세요.

- 다른 USB 케이블과 컴퓨터의 다른 USB 포트를 사용해 보세요.

- Kobo에서 로그아웃한 뒤 다시 로그인하세요. 이렇게 하면 데이터베이스를 다시 빌드하여 손상된 데이터베이스 오류를 해결할 수 있습니다.

- Kobo Touch의 펌웨어를 최신 버전으로 업그레이드해 보세요.

- Kobo를 초기화해 보세요(가끔은 잠시 문제가 해결되지만 다시 나타날 수 있으며, 그런 경우 다시 초기화해야 합니다).

- 한 번에 Kobo에 한두 권만 넣어 보고, Kobo에 큰 컬렉션을 보관하지 마세요.

내가 e-ink Kindle로 보낸 책의 표지가 잠깐 나타났다가 일반 표지로 바뀝니다?

이것은 Amazon의 버그 때문에 발생합니다. Amazon은 자기 서버에서 책 표지를 내려받으려 시도하고, 실패하면 calibre가 만든 기존 표지를 일반 표지로 바꿔 버립니다. 자세한 내용은 이 포럼 글 [https://www.mobileread.com/forums/showthread.php?t=329945] 을 보세요. calibre 4.17부터는 Kindle을 calibre에 다시 연결하면 Amazon이 파괴한 표지를 calibre가 자동 복원하는 우회 방법이 있습니다. 따라서 Kindle에서 표지를 보려면 다음을 하세요:

1. calibre로 책을 Kindle에 전송하세요.

2. Kindle을 분리하고 Amazon이 표지를 덮어쓸 때까지 기다리세요.

3. Kindle을 다시 calibre에 연결하세요.

이 우회 방법은 calibre 4.17 이상으로 보낸 책에만 동작합니다. 또는 Kindle을 비행기 모드로 두세요. 어차피 Amazon이 당신이 읽는 모든 책을 아는 것은 원하지 않을 것입니다. Amazon 고객 지원에 연락해 이 버그에 대해 강하게 항의하는 것도 권합니다. Amazon이 들을지도 모릅니다.

참고

우회 방법이 동작하지 않는다면 Kindle 펌웨어가 2020년 4월 릴리스된 5.12.5 이상인지 확인하세요.

Kindle ColorSoft 이상으로 보낸 책의 표지가 책 목록에 나타나지 않습니다?

Amazon은 ColorSoft 및 이후 장치에서 사용자가 자사 장치에서 Amazon 외 책을 읽지 못하게 하려는 목적으로 이 기능을 의도적으로 망가뜨렸습니다. 자세한 내용은 이 포럼 글 [https://www.mobileread.com/forums/showthread.php?t=364350] 을 보세요. 알려진 유일한 우회 방법은 책을 Kindle로 “개인 문서”로 보내는 것입니다. 이렇게 하면 표지 문제는 해결되지만 Whispersync 같은 다른 기능은 깨지고 장치에서는 해당 책이 “책”이 아니라 “개인 문서” 아래에 표시됩니다. calibre에서 이 기능을 켜려면 환경설정 → 출력 옵션 → MOBI 출력 으로 가서 책 내용 공유 활성화 체크 상자를 켜세요. 그러면 calibre가 이후 Kindle로 보내는 모든 책이 개인 문서로 표시됩니다.

내 MOBI 파일 표지가 Kindle for PC/Kindle for Android/iPad 등에서 더 이상 보이지 않습니다.

이것은 Amazon 소프트웨어의 버그 때문에 생깁니다. 환경설정 → 변환 → 출력 옵션 → MOBI 출력 으로 가서 책 내용 공유 활성화 옵션을 설정하면 우회할 수 있습니다. 이전에 변환한 책을 다시 변환하는 경우에는 해당 개별 책의 변환 대화상자에서도 이 옵션을 켜야 합니다(책별 변환 설정이 저장되며 우선 적용되기 때문입니다).

이렇게 하면 생성된 MOBI가 Kindle Fire에서 “책”이 아니라 개인 문서로 표시되고 Amazon whispersync는 동작하지 않지만 표지는 보이게 됩니다. 어느 기능이 더 중요한지는 사용자 선택입니다. Amazon에 연락해서 이 버그를 고쳐 달라고 요청하는 것을 권합니다.

Amazon 소프트웨어의 버그는 MOBI 파일을 Kindle에 저장할 때 파일이 개인 문서로 표시되지 않는 한 Amazon은 사용자가 책을 구입했다고 가정하고 서버에서 해당 책의 표지 썸네일을 다운로드하려고 시도한다는 것입니다. 다운로드에 실패하면 MOBI 파일에 정의된 표지로 대체를 거부합니다. 이것은 Amazon이 저자를 통해서만 판매하도록 강요하려는 의도일 가능성이 높습니다. 즉, Kindle은 개인 문서로 표시된 책이나 Amazon에서 직접 구입한 책의 표지만 표시합니다.

USB 연결을 사용하여 MOBI 파일을 캘리버가 있는 전자 잉크 Kindle로 보내는 경우, 캘리버는 표지 썸네일 자체를 업로드하여 이 Amazon 버그를 해결합니다. 그러나 이 해결 방법은 USB 연결을 사용하고 캘리버로 전송할 때만 가능합니다. 이메일을 사용하여 보내는 경우, Amazon은 자동으로 MOBI 파일을 개인 문서로 표시하고 표지는 만들지만 책은 개인 문서에 표시됩니다.

calibre로 Kindle에 책 몇 권을 전송했는데 나타나지 않습니다?

Kindle로 보낸 책은 Kindle이 그 책을 *색인화*한 뒤에야 표시됩니다. 이 작업에는 시간이 걸릴 수 있습니다. 시간이 좀 지난 뒤에도 책이 보이지 않는다면 Kindle 색인기가 충돌했을 가능성이 큽니다. 특정 책 하나가 색인기를 충돌시키는 경우도 있습니다. 안타깝게도 Amazon은 어떤 책이 Kindle에서 충돌을 일으키는지 알아낼 방법을 제공하지 않았습니다. 해결책은 Kindle을 초기화하거나 Windows 탐색기(또는 사용하는 파일 관리자)로 Kindle 메모리의 파일을 모두 지운 뒤 책을 하나씩 다시 보내면서 문제 책을 찾는 것뿐입니다. 문제 책을 찾으면 Kindle에서 그 책을 삭제하고 calibre에서 MOBI→MOBI 또는 MOBI→AZW3 변환을 한 다음 다시 보내세요. 대부분 이 방법으로 문제가 해결됩니다.

라이브러리 관리

목차

- 책 파일은 어디에 저장되나요?

- 캘리버는 저자 이름과 정렬을 어떻게 관리합니까?

- 왜 calibre는 내가 원하는 폴더 구조에 책을 저장하지 못하게 하나요?

- 왜 calibre에는 foo용 열이 없나요?

- 형식이나 ISBN을 표시하는 열을 만들 수 있나요?

- calibre 데이터를 한 컴퓨터에서 다른 컴퓨터로 옮기려면 어떻게 하나요?

- calibre의 책 목록이 비어 있습니다!

- 네트워크 드라이브/NAS의 내 캘리버 라이브러리에 오류가 발생합니까?

책 파일은 어디에 저장되나요?

calibre를 처음 실행하면 책을 저장할 폴더를 물어봅니다. calibre에 책을 추가할 때마다 책 파일은 그 폴더로 복사됩니다. 폴더 안의 책은 저자와 제목별 하위 폴더로 정리됩니다. 이 폴더의 내용은 calibre가 자동으로 관리하므로 이 폴더에 파일/폴더를 수동으로 추가하면 안 됩니다. 자동으로 삭제될 수 있기 때문입니다. 특정 책과 연결된 파일을 추가하려면 메타데이터 편집 대화상자 오른쪽 위 영역을 사용하세요. 그러면 calibre가 그 파일을 올바른 폴더에 자동으로 넣고 제목/저자가 바뀔 때 함께 이동시킵니다.

책 메타데이터는 라이브러리 폴더 최상위에 있는 metadata.db 파일에 저장됩니다. 이 파일은 sqlite 데이터베이스입니다. 라이브러리를 백업할 때는 전체 폴더와 모든 하위 폴더를 복사해야 합니다.

라이브러리 폴더와 그 안의 모든 내용이 바로 calibre 라이브러리입니다. 이런 라이브러리는 여러 개 가질 수 있습니다. 라이브러리를 관리하려면 도구 모음에서 calibre 아이콘을 클릭하세요. 새 라이브러리를 만들고, 기존 라이브러리를 제거/이름 변경하고, 라이브러리 간 전환도 쉽게 할 수 있습니다.

서로 다른 라이브러리 사이에서(라이브러리를 두 개 이상 설정한 뒤에는) 책을 복사하거나 이동할 수 있습니다. 책을 오른쪽 클릭한 뒤 라이브러리로 복사 동작을 선택하세요.

캘리버는 저자 이름과 정렬을 어떻게 관리합니까?

저자 이름은 특히 문화권마다 매우 복잡합니다. 몇 가지 복잡한 사례는 이 참고 글 [https://www.w3.org/International/questions/qa-personal-names.en.php?changelang=en] 을 보세요. calibre는 저자 이름을 매우 유연하게 관리합니다. 먼저 이해해야 할 것은 calibre에서 책과 저자는 별개의 개체라는 점입니다. 한 책에는 여러 저자가 있을 수 있고, 한 저자는 여러 책을 가질 수 있습니다. 책의 저자는 메타데이터 편집 대화상자에서 관리할 수 있습니다. 개별 저자는 calibre 기본 창 왼쪽 태그 브라우저에서 해당 저자를 오른쪽 클릭하고 저자 관리 를 선택해 관리할 수 있습니다. 이 대화상자에서는 저자 이름과 정렬 방식을 바꿀 수 있습니다. 그러면 해당 저자의 모든 책에서 저자 이름이 자동으로 바뀝니다. 책에 여러 저자가 있으면 & 문자로 이름을 구분하세요.

이제 저자 이름 정렬하기가 시작됩니다:

- 새 저자가 calibre에 추가되면(새 저자의 책을 추가할 때 일어납니다) calibre는 책과 저자 모두에 대해 정렬 문자열을 자동 계산합니다.

- 태그 브라우저의 저자는 **저자들**의 정렬 값으로 정렬됩니다. 이것이 책의 Author sort 필드와는 다르다는 점을 기억하세요.

- 기본적으로 이 정렬 알고리즘은 저자 이름이 이름 성 형식이라고 가정하고 성, 이름 정렬 값을 생성합니다.

- 이 알고리즘은 환경설정 → 고급 → 트윅 으로 가서 author_sort_copy_method 트윅을 설정해 바꿀 수 있습니다.

- 아무 저자나 오른쪽 클릭해 저자 관리 를 선택한 뒤 모든 저자 정렬 값 다시 계산 버튼을 누르면 모든 저자의 정렬 값을 강제로 다시 계산할 수 있습니다. 이 작업은 원하는 값으로 author_sort_copy_method 트윅을 설정한 뒤 하세요.

- 일괄 메타데이터 편집 대화상자(모든 책을 선택하고 메타데이터 편집을 누른 뒤 저자 정렬 자동 설정 체크 상자를 켜고 확인)를 사용하면 모든 책의 저자 정렬 값을 강제로 다시 계산할 수 있습니다.

- 책의 저자 정렬 값을 다시 계산할 때 calibre는 각 개별 저자의 정렬 값을 사용합니다. 따라서 책의 저자 정렬 값을 다시 계산하기 전에 개별 저자의 정렬 값이 올바른지 확인하세요.

- 태그 브라우저가 저자를 이름으로 표시할지 정렬 값으로 표시할지는 환경설정 → 고급 → 트윅 의 categories_use_field_for_author_name 트윅으로 제어할 수 있습니다.

:guilabel:`저자 관리`를 사용하여 개별 저자의 정렬 값을 원하는 대로 설정할 수 있습니다. 이것은 Miguel de Cervantes Saavedra와 같은 복잡한 다중 부분 이름이나 Sun Tzu와 같은 아시아 이름을 다룰 때와 같이 캘리버가 올바르지 않은 이름을 다룰 때 유용합니다.

이 정도 유연성이 있으므로 calibre가 저자 이름을 원하는 방식으로 관리하게 할 수 있습니다. 예를 들어 흔한 요청 중 하나는 calibre에 저자 이름을 LN, FN 형식으로 표시하게 하는 것입니다. 이렇게 하려면, 그리고 아래 참고가 해당되지 않는다면 다음과 같이 하세요:

- 위에서 설명한 대로 author_sort_copy_method 조정을 ``복사하기``로 지정합니다.

- 캘리버를 다시 시작합니다. 나머지 단계를 수행하기 전에 책의 메타데이터를 변경하지 마십시오.

- 저자 관리 대화상자를 사용해 모든 저자 이름을 LN, FN 형식으로 변경합니다.

- 모든 저자를 변경한 후 ‘모든 저자 정렬 값 다시 계산’ 버튼을 누릅니다.

- 확인을 누르면 calibre가 모든 책의 저자를 변경합니다. 이 작업에는 시간이 걸릴 수 있습니다.

참고

FN LN에서 LN, FN으로 바꿀 때 author_sort 값은 이미 LN, FN 형식으로 되어 있는 경우가 많습니다. 당신의 경우가 그렇다면 다음과 같이 하세요:

- 위에서 설명한 대로 author_sort_copy_method 조정을 ``복사하기``로 지정합니다.

- 캘리버를 다시 시작합니다. 나머지 단계를 수행하기 전에 책의 메타데이터를 변경하지 마십시오.

- 저자 관리 대화상자를 엽니다. 모든 author sort 값을 author로 복사 버튼을 누르세요.

- 저자 목록을 검토해 만족스러운지 확인하세요. 아직은 취소를 눌러 변경을 버릴 수 있습니다. 확인을 누르면 실행 취소가 없습니다.

- 확인을 누르면 calibre가 모든 책의 저자를 변경합니다. 이 작업에는 시간이 걸릴 수 있습니다.

왜 calibre는 내가 원하는 폴더 구조에 책을 저장하지 못하게 하나요?

calibre 라이브러리 관리 기능의 핵심은 사용자가 어떤 폴더 체계를 짜더라도 그것보다 훨씬 효율적인 검색 및 정렬 기반 인터페이스로 책을 찾을 수 있게 하는 데 있습니다. 실제로 calibre 인터페이스로 컬렉션을 찾고, 정렬하고, 훑는 데 익숙해지면 디스크 파일을 뒤져 책을 찾을 필요를 다시는 느끼지 않게 될 것입니다. calibre는 저자 -> 제목 -> 책 파일 형태의 자체 폴더 구조로 책을 관리함으로써 높은 수준의 신뢰성과 표준화를 달성할 수 있습니다. 검색/태그 기반 인터페이스가 폴더보다 우수한 이유를 설명하기 위해 다음을 생각해 보세요. 예를 들어 당신의 책 컬렉션이 다음과 같은 체계로 폴더에 잘 정리되어 있다고 합시다:

Genre -> Author -> Series -> ReadStatus

이렇게 하면 예를 들어 Foundation 시리즈의 Isaac Asimov가 쓴 모든 SF 책을 찾는 것은 매우 쉽습니다. 하지만 읽지 않은 SF 책을 모두 찾고 싶다면 어떨까요? 이 폴더 구조로는 쉽게 할 방법이 없습니다. 대신 다음과 같은 폴더 구조가 필요할 것입니다:

ReadStatus -> Genre -> Author -> Series

캘리버에서는 대신 태그를 사용하여 장르와 읽기 상태를 표시한 다음 ``tag:scifi가 아닌 tag:read``와 같은 간단한 검색어를 사용합니다. 캘리버에는 멋진 그래픽 인터페이스도 있으므로 검색 언어를 배울 필요가 없습니다. 대신 태그를 클릭하여 검색에 포함하거나 제외할 수 있습니다.

파일시스템에 접근해야 책을 네트워크로 접근할 수 있다고 주장하는 분들을 위해 말하자면, calibre에는 인터넷을 통해 calibre 라이브러리에 접근할 수 있게 해주는 훌륭한 콘텐츠 서버가 있습니다.

언젠가 캘리버의 개발이 중단되고 모든 책이 폴더 구조에 고정되어 있지 않을까 걱정된다면, 그 메타데이터를 기반으로 모든 파일을 임의의 복잡성의 폴더 구조로 내보낼 수 있는 강력한 :guilabel:’디스크에 저장’ 특정 기능을 살펴보십시오.

마지막으로 모든 제목 폴더 끝에 숫자가 붙는 이유는 견고성 때문입니다. 그 숫자는 calibre 데이터베이스에 있는 책 레코드의 ID 번호입니다. 이 숫자가 있으면 같은 제목과 같은 저자 이름을 가진 레코드를 여러 개 둘 수 있습니다. 또한 데이터베이스 파일이 손상되었을 때 calibre가 모든 메타데이터를 포함해 데이터베이스를 마법처럼 다시 생성할 수 있는 이유 중 하나이기도 합니다. calibre의 목표는 파일 이름에 메타데이터를 저장하지 않게 하고, 파일시스템으로 물건을 찾지 않게 하는 것이므로, 폴더 이름이 덜 예뻐지는 대신 얻는 안정성 향상은 충분히 가치가 있습니다.

그래도 납득이 되지 않는다면 안타깝지만 calibre는 당신에게 맞는 도구가 아닙니다. 책 목록 관리용으로는 다른 도구를 찾아보세요. 분명히 말하지만, 이것은 바뀌지 않습니다. 이것을 바꾸라고 우리에게 연락하지 말아 주세요.

왜 calibre에는 foo용 열이 없나요?

calibre는 가장 자주, 가장 널리 쓰이는 필드에 대한 열을 제공하도록 설계되었습니다. 그 외에도 원하는 열은 얼마든지 추가할 수 있습니다. 열은 환경설정 → 인터페이스 → 사용자 열 추가 에서 추가할 수 있습니다. 나만의 열을 만드는 방법은 UI Power tips [https://calibre-ebook.com/ko/demo#tutorials] 튜토리얼을 보거나 이 블로그 글 [https://blog.calibre-ebook.com/calibre-custom-columns/] 을 읽어 보세요.

다른 열의 메타데이터를 조합한 “가상 열”도 만들 수 있습니다. 열 추가 대화상자에서 빠른 생성 링크를 사용하면 책 ISBN이나 형식을 보여 주는 열을 쉽게 만들 수 있습니다. 강력한 calibre 템플릿 언어를 사용하면 열로 훨씬 더 많은 일을 할 수 있습니다. 자세한 내용은 calibre 템플릿 언어 를 보세요.

형식이나 ISBN을 표시하는 열을 만들 수 있나요?

네, 가능합니다. 사용자 지정 열을 추가하려면 위 답변의 지침을 따르십시오.

calibre 데이터를 한 컴퓨터에서 다른 컴퓨터로 옮기려면 어떻게 하나요?

모든 calibre 데이터(책, 설정, 플러그인)를 내보낸 뒤 다른 컴퓨터에서 가져올 수 있습니다. 먼저 데이터 내보내기 방법부터 보겠습니다:

- 기본 캘리버 도구모음에서 캘리버 아이콘을 마우스 오른쪽 버튼으로 클릭하고 모든 캘리버 데이터 내보내기/가져오기`를 선택합니다. 현재 연결된 장치가 있는 경우 이 메뉴 옵션을 사용할 수 없으므로 연결된 장치를 모두 분리하십시오. 그런 다음 :guilabel:`모든 캘리버 데이터 내보내기 버튼을 클릭합니다. 모든 캘리버 책 정보 목록이 표시됩니다. 확인을 클릭하고 컴퓨터의 빈 폴더를 선택합니다. 내보낸 데이터는 이 폴더에 저장됩니다. 이 폴더를 새 컴퓨터에 복사하고 아래 지침에 따라 데이터를 가져오기만 하면 됩니다.

- 새 컴퓨터에 캘리버를 설치하고 시작 마법사`를 통해 실행합니다. 다음 단계에서 이전 설정을 가져오기 때문에 여기서 무엇을 하든 상관 없습니다. 이제 라이브러리에 :guilabel:`시작하기 가이드만 있는 빈 캘리버를 갖게 됩니다. 다시 한 번 캘리버 버튼을 마우스 오른쪽 버튼으로 클릭하고 모든 캘리버 데이터 내보내기/가져오기`를 선택합니다. 그런 다음 :guilabel:`이전에 내보낸 데이터 가져오기 버튼을 클릭합니다. 이전에 복사한 내보낸 데이터가 있는 폴더를 선택합니다. 이제 가져올 수 있는 라이브러리 목록이 표시됩니다. 목록을 하나씩 살펴보고 각 라이브러리의 새 위치를 선택합니다(위치는 컴퓨터 어딘가에 있는 빈 폴더일 뿐입니다). 확인을 클릭합니다. 가져오기가 완료되면 모든 이전 라이브러리, 설정 및 캘리버 플러그인과 함께 캘리버가 다시 시작됩니다.

참고

이 가져오기/내보내기 기능은 calibre 2.47 이상에서만 사용할 수 있습니다. calibre 버전이 더 오래되었거나 가져오기/내보내기에 문제가 생긴다면, 다음 문단에서 설명하는 것처럼 calibre 라이브러리 폴더를 수동으로 복사해도 됩니다.

기존 컴퓨터에서 새 컴퓨터로 calibre 라이브러리 폴더를 그대로 복사하면 됩니다. 라이브러리 폴더 위치는 도구 모음의 calibre 아이콘을 클릭해 확인할 수 있습니다. calibre 라이브러리 전환/만들기 동작을 선택하면 현재 calibre 라이브러리 경로가 표시됩니다.

이제 새 컴퓨터에서 calibre를 처음 시작하세요. 그러면 환영 마법사 가 실행되며 calibre 라이브러리 위치를 물어봅니다. 방금 복사한 폴더를 지정하세요. 이미 해당 컴퓨터에 calibre가 설치되어 있다면 환영 마법사 는 실행되지 않습니다. 그런 경우 도구 모음의 calibre 아이콘을 오른쪽 클릭하고 새로 복사한 폴더를 지정하세요. 그러면 컴퓨터에 calibre 라이브러리가 두 개 있게 되며 도구 모음의 calibre 아이콘을 눌러 둘 사이를 전환할 수 있습니다. 이 방식으로 라이브러리를 옮기면 모든 메타데이터, 태그, 사용자 열 등이 그대로 보존됩니다.

calibre의 책 목록이 비어 있습니다!

왜 이런 일이 일어났는지 이해하려면 calibre 라이브러리가 무엇인지 이해해야 합니다. 가장 기본적으로 calibre 라이브러리는 그냥 폴더입니다. calibre에 책을 추가할 때마다 그 책 파일은 이 폴더로 복사됩니다(저자와 제목 기준 하위 폴더로 정리됨). calibre 라이브러리 폴더 안의 최상위에는 metadata.db라는 파일이 있습니다. 이 파일이 calibre가 라이브러리의 모든 책에 대한 제목/저자/평점/태그 등의 메타데이터를 저장하는 곳입니다. calibre가 표시하는 책 목록은 이 metadata.db 파일 내용을 읽어서 만들어집니다.

calibre가 빈 책 목록을 표시하는 이유는 두 가지가 있습니다:

- calibre 라이브러리 폴더의 위치가 바뀌었습니다. 외장 디스크에 있었는데 드라이브 문자가 바뀌었거나, 실수로 폴더를 이동했을 수 있습니다. 이 경우 calibre는 라이브러리를 찾을 수 없어 빈 라이브러리로 시작합니다. 해결하려면 calibre 도구 모음의 calibre 아이콘을 오른쪽 클릭하고 라이브러리 전환/만들기를 선택하세요. 작은 파란 아이콘을 눌러 calibre 라이브러리의 새 위치를 선택하고 확인을 누르세요. 새 위치를 모르면 컴퓨터에서 metadata.db 파일을 검색하세요.

- metadata.db 파일이 삭제되었거나 손상되었습니다. 이 경우 calibre에 백업에서 metadata.db를 다시 만들도록 지시할 수 있습니다. calibre 도구 모음의 calibre 아이콘을 오른쪽 클릭하고 라이브러리 유지보수->데이터베이스 복원을 선택하세요. calibre가 metadata.db를 자동으로 다시 빌드합니다.

네트워크 드라이브/NAS의 내 캘리버 라이브러리에 오류가 발생합니까?

캘리버 라이브러리를 네트워크 드라이브에 두지 마십시오.

파일시스템은 매우 복잡한 존재입니다. 대부분의 네트워크 파일시스템은 calibre가 사용하는 여러 기능을 지원하지 않습니다. 어떤 것은 파일 잠금을 지원하지 않고, 어떤 것은 하드링크를 지원하지 않으며, 어떤 것은 그냥 불안정합니다. 게다가 calibre는 단일 사용자용 애플리케이션이므로 같은 네트워크 라이브러리에 calibre 두 사본을 실수로 동시에 실행하면 나쁜 일이 생깁니다. 마지막으로 운영체제마다 파일시스템 제한도 다르므로 네트워크 드라이브를 여러 운영체제에서 공유하면 역시 문제가 반드시 생깁니다.

다른 컴퓨터에서 책을 사용하려면 calibre 콘텐츠 서버를 쓰는 것을 고려하세요. calibre는 한 컴퓨터에서만 실행하고, 다른 곳에서는 콘텐츠 서버나 원격 데스크톱 솔루션을 통해 접근하세요.

실제 라이브러리를 반드시 공유해야 한다면 네트워크 드라이브 대신 DropBox나 rsync 같은 파일 동기화 도구를 사용하세요. 파일 동기화 도구를 쓸 때는 calibre와 동기화 도구가 동시에 calibre 라이브러리에 접근하지 않도록 하는 것이 필수 입니다. 즉 파일 동기화 도구와 calibre를 동시에 실행하면 안 됩니다. calibre 라이브러리는 사용자 간에 공유해서는 안 되며, 신뢰할 수 없는 출처에서 받은 calibre 라이브러리는 절대로 열지 마세요. 보안 위험이 있습니다.

이런 도구를 사용해도 데이터 손상/손실 위험은 있으므로, 그 위험을 감수할 의향이 있을 때만 사용하세요. 특히 Google Drive 는 calibre와 호환되지 않습니다. calibre 라이브러리를 Google Drive에 두면 데이터 손실이 발생합니다. 자세한 내용은 이 스레드 [https://www.mobileread.com/forums/showthread.php?t=205581] 를 보세요.

기타

목차

- Amazon이 MOBI 파일 이메일 전송을 중단하나요?

- 내가 좋아하는 뉴스 사이트에서 calibre가 뉴스를 내려받게 하고 싶습니다.

- 이름을 왜 calibre라고 지었나요?

- macOS에서는 왜 일부 글꼴만 calibre에 표시되나요?

- Windows에서 calibre가 시작되지 않습니다?

- calibre가 가끔 멈추거나 충돌합니다?

- 뷰어를 사용하거나 변환을 하면 Windows에서 permission denied 오류가 납니다.

- macOS에서 calibre가 시작되지 않거나 충돌합니다?

- calibre 전자책 뷰어를 실행하면 검은 화면 또는 흰 화면만 나옵니다?

- 설치 프로그램을 내려받았는데 동작하지 않습니다?

- 내 백신 프로그램이 calibre를 바이러스/트로이 목마라고 주장합니다?

- calibre는 어떻게 백업하나요?

- 구입한 EPUB 책을 calibre와 함께 사용하려면 어떻게 하나요(.acsm 파일은 어떻게 하나요)?

- “권한 거부됨” 오류가 발생합니까?

- 댓글 메타데이터를 리더기에서도 보이게 할 수 있나요?

- calibre가 내 HTTP 프록시를 사용하게 하려면 어떻게 하나요?

- calibre에 어떤 기능을 추가하고 싶습니다. 어떻게 해야 하나요?

- 왜 calibre에는 자동 업데이트가 없나요?

- calibre는 어떤 라이선스로 배포되나요?

- USB 메모리에서 calibre를 실행하려면 어떻게 하나요?

- 내 Linux 서버에서 뉴스 다운로드나 콘텐츠 서버 같은 calibre 기능 일부를 실행하려면 어떻게 하나요?

Amazon이 MOBI 파일 이메일 전송을 중단하나요?

Amazon은 @kindle.com 이메일 주소로 보낸 MOBI 파일을 더 이상 받지 않겠다고 발표했습니다 [https://blog.the-ebook-reader.com/2022/05/03/amazon-dropping-mobi-support-on-send-to-kindle-apps/]. calibre에서 환경설정 → 이메일로 책 공유 로 가서 @kindle.com 주소로 보낼 형식 목록에서 MOBI를 제거하고 EPUB를 추가하면 MOBI 대신 EPUB를 보내도록 설정할 수 있습니다.

하지만 Amazon의 EPUB 수용 방식은 매우 문제가 많아서 다른 곳에서는 모두 잘 동작하는 EPUB도 상당수 거부합니다. 이런 경우 다음 요령을 시도할 수 있습니다:

1. EPUB 파일을 MOBI로 변환합니다.

2. 그다음 MOBI 파일을 다시 EPUB로 변환하고, 그 결과 EPUB 파일을 보냅니다.

이렇게 하면 고급 서식, 내장 글꼴 등은 모두 제거되지만 Amazon이 EPUB를 받아들일 가능성은 크게 높아집니다.

참고

이전에 calibre가 내려받은 정기간행물을 이메일로 보냈다면 USB 케이블로 보내거나 Kindle 내장 브라우저를 통해 calibre 콘텐츠 서버에서 다운로드하는 편이 더 낫습니다. 그래도 이메일 전송을 계속 쓰고 싶다면 환경설정->동작에서 출력 형식을 EPUB로 바꿔 보세요. 그러면 calibre가 뉴스를 EPUB 형식으로 내려받습니다. Amazon이 그 EPUB를 받아들일지는 또 다른 문제입니다.

내가 좋아하는 뉴스 사이트에서 calibre가 뉴스를 내려받게 하고 싶습니다.

컴퓨터에 어느 정도 익숙하다면 원하는 웹사이트에서 뉴스를 내려받도록 calibre를 직접 가르칠 수 있습니다. 방법은 즐겨찾는 뉴스 웹사이트 추가 를 보세요.

그렇지 않다면 calibre Recipes 포럼 [https://www.mobileread.com/forums/forumdisplay.php?f=228] 에 글을 올려 특정 뉴스 사이트를 요청할 수 있습니다.

이름을 왜 calibre라고 지었나요?

마음에 드는 것을 고르세요:

- Converter And LIBRary for E-books

- 수준 높은(calibre) 제품

- 최초의 전자잉크 기반 전자책 리더였던 SONY Librie에 대한 헌사

- 제 아내가 골랐습니다 ;-)

캘리버는 ca-li-bre가 아니라 cal-i-ber로 발음됩니다. 궁금하다면 calibre는 caliber의 영국/영연방 철자법입니다. 인도인으로서, 그것이 제 자연스러운 철자법입니다.

macOS에서는 왜 일부 글꼴만 calibre에 표시되나요?

calibre는 자신이 만드는 전자책 파일에 글꼴을 포함합니다. 전자책 파일은 TrueType 및 OpenType(.ttf, .otf) 글꼴만 포함할 수 있습니다. macOS 시스템의 많은 글꼴은 .dfont 형식이라 포함할 수 없습니다. calibre는 시스템에서 찾은 TrueType 및 OpenType 글꼴만 표시합니다. 이런 글꼴은 웹에서 많이 구할 수 있습니다. .ttf/.otf 파일을 내려받아 홈 폴더의 Library/Fonts 폴더에 추가하면 됩니다.

Windows에서 calibre가 시작되지 않습니다?

원인은 여러 가지일 수 있습니다:

- 오류는 없지만 calibre 창이 나타나지 않는다면 화면 밖에 떠 있을 가능성이 큽니다. 여기 [https://www.wikihow.com/Bring-an-Off-Screen-Window-Back-on-Windows] 에 설명된 방법 중 하나로 모든 창을 현재 화면으로 가져올 수 있습니다.

- 일부 소프트웨어는 calibre와 충돌하는 것으로 알려져 있습니다. 안전 모드로 재부팅해서 동작하는지 확인해 보세요. 알려진 원인 중 하나는 Sunshine [https://github.com/LizardByte/Sunshine] 화면 공유 소프트웨어입니다.

- 다른 프로그램이 파일을 사용 중이라 calibre가 파일을 열 수 없다는 오류가 나오면 다음을 하세요:

- calibre를 제거합니다.

- 컴퓨터를 재부팅합니다.

- calibre를 다시 설치합니다. 단 설치 마법사에서 calibre를 바로 시작하지는 마세요.

- 잠시 백신 프로그램을 비활성화합니다(안전을 위해 먼저 인터넷 연결을 끊으세요).

- calibre 라이브러리로 선택한 폴더를 열어보세요. metadata.db라는 파일이 있으면 삭제하세요.

- calibre를 시작합니다.

- 이제부터는 calibre를 정상적으로 시작할 수 있을 것입니다.

- calibre 업그레이드 후 Python 함수가 예기치 않게 종료되었다는 오류가 나면 먼저 calibre를 제거한 뒤 C:Program FilesCalibre 및 C:Program FilesCalibre2 폴더가 있으면 삭제하세요. 그런 다음 다시 설치하면 괜찮을 것입니다.

- calibre 최초 실행 시 환영 마법사 에서 오류가 나면 calibre 라이브러리로 C:library 같은 폴더를 선택해 보세요(calibre는 경로에 영문이 아닌 문자가 있거나 숫자만 있는 경우 라이브러리 위치 처리에 문제가 생기기도 합니다).

- 관리자 권한으로 실행해 보세요(아이콘을 오른쪽 클릭하고 관리자 권한으로 실행 선택).

그래도 시작되지 않으면 명령 프롬프트를 시작하세요(Windows 키와 R을 누른 뒤 나타나는 실행 대화상자에 cmd.exe 입력). 명령 프롬프트에서 다음 명령을 입력하고 Enter를 누르세요:

calibre-debug -g

표시되는 모든 출력을 포럼 [https://www.mobileread.com/forums/forumdisplay.php?f=166] 의 도움말 글에 올리세요.

calibre가 가끔 멈추거나 충돌합니다?

제가 아는 범위에서 이런 문제를 일으킬 수 있는 원인은 여러 가지가 있습니다:

- 최근 외부 모니터나 TV를 컴퓨터에 연결했습니다. 이 경우 calibre가 메타데이터 편집 창이나 변환 대화상자 같은 새 창을 열 때 그 창이 보이지 않는 두 번째 모니터에 나타나서 calibre가 멈춘 것처럼 보일 수 있습니다. 두 번째 모니터를 분리하고 calibre를 다시 시작하세요.

- 다음 프로그램들은 calibre에서 충돌을 일으키는 것으로 보고되었습니다. 이들 중 하나를 실행 중이라면 calibre를 시작하기 전에 닫거나 제거하세요: RoboForm, Logitech SetPoint Settings, Constant Guard Protection by Xfinity, Spybot, Killer Network Manager, Nahimic UI Interface, Acronis True Image.

- Wacom 브랜드 USB 마우스/태블릿을 사용 중입니다. Wacom 드라이버와 calibre가 사용하는 그래픽 툴킷 사이에 비호환성이 있습니다. Wacom이 아닌 마우스를 사용해 보세요.

- 일부 64비트 Windows 버전에서는 보안 소프트웨어/설정 때문에 64비트 calibre가 제대로 동작하지 않습니다. 64비트 calibre를 사용 중이라면 32비트 버전으로 바꿔 보세요.

- calibre 전자책 뷰어에서 텍스트를 복사하려 할 때 충돌이 난다면, 대개 클립보드 감시/관리 앱이 원인입니다. 그것을 끄면 괜찮아질 것입니다.

- 책 추가 버튼이나 디스크에 저장 버튼을 누를 때처럼 파일 대화상자를 사용할 때 충돌이 난다면, 컴퓨터에 손상된 셸 확장을 설치한 소프트웨어가 있는 것입니다. 알려진 원인으로는 SpiderOak, odrive sync, Dell Backup and Recovery, NetDrive 가 있습니다. 이런 프로그램이 있다면 제거하면 해결됩니다. 전체 프로그램을 제거하고 싶지 않다면 NirSoft Shell Extension Viewer [https://www.nirsoft.net/utils/shexview.html] 로 시스템에 설치된 셸 확장을 보고 개별적으로 비활성화할 수도 있습니다. 셸 확장을 비활성화한 뒤에는 반드시 “Restart Explorer”를 실행하거나 컴퓨터를 재부팅하세요.

위 사항이 모두 해당되지 않는다면 컴퓨터의 다른 프로그램이 calibre를 방해하고 있는 것입니다. 먼저 실행 중인 프로그램을 최대한 줄이기 위해 안전 모드로 재부팅한 뒤 충돌이 계속 나는지 확인하세요. 충돌이 사라지면 어떤 프로그램이 문제 원인이라는 뜻입니다. 가장 가능성이 높은 것은 백신, 장치 드라이버, RoboForm 같은 다른 프로그램의 동작을 바꾸는 프로그램, 음성 제어나 화면 읽기 같은 보조 기술입니다.

원인을 찾는 유일한 방법은 프로그램을 하나씩 제외해 보며 어떤 프로그램이 문제를 일으키는지 확인하는 것입니다. 기본적으로 프로그램 하나를 중지하고 calibre를 실행해 충돌을 확인하세요. 계속 충돌하면 다른 프로그램 하나를 더 중지하고 반복하세요.

뷰어를 사용하거나 변환을 하면 Windows에서 permission denied 오류가 납니다.

컴퓨터의 무언가가 calibre가 자신의 임시 파일에 접근하는 것을 막고 있습니다. 가장 가능성이 높은 원인은 Temp 폴더 권한이 잘못된 것입니다. Windows 탐색기에서 C:UsersUSERNAMEAppDataLocal 폴더로 가서 Temp 폴더를 오른쪽 클릭하고 속성 을 선택한 다음 보안 탭으로 가세요. 사용자 계정이 이 폴더에 대해 전체 제어 권한을 가지고 있는지 확인하세요.

일부 사용자는 관리자 권한 명령 프롬프트에서 다음 명령을 실행하면 권한 문제가 해결되었다고 보고했습니다. 관리자 권한 명령 프롬프트를 열려면 시작 메뉴에서 cmd.exe를 검색한 뒤 명령 프롬프트 항목을 오른쪽 클릭하고 관리자 권한으로 실행 을 선택하세요. 명령 프롬프트에서 다음 명령을 입력하고 Enter를 누르세요:

icacls "%appdata%\..\Local\Temp" /reset /T

또는 calibre를 관리자 권한으로 실행할 수도 있지만, 그렇게 하면 드래그 앤 드롭 같은 일부 기능이 동작하지 않습니다.

마지막으로 일부 사용자는 UAC를 끄면 문제가 해결된다고 보고했습니다.

macOS에서 calibre가 시작되지 않거나 충돌합니다?

macOS에서 실패하는 흔한 원인 중 하나는 calibre가 사용하는 그래픽 툴킷과 호환되지 않는 접근성 기술을 사용하는 것입니다. VoiceOver를 켜 두었다면 꺼 보세요. 또한 시스템 환경설정->손쉬운 사용으로 가서 모든 탭에서 보조 장치 접근 허용 설정을 끄세요. 또 다른 원인은 Smart Scroll, Default Folder X처럼 시스템 동작을 바꾸는 서드파티 앱일 수 있습니다.

calibre가 왜 시작되지 않는지에 대한 디버그 출력은 Console.app 을 실행해 얻을 수 있습니다. 디버그 출력은 거기에 표시됩니다. 그 출력에 다음과 같은 줄이 들어 있다면:

Qt: internal: -108: Error ATSUMeasureTextImage text/qfontengine_mac.mm

문제는 손상된 글꼴 캐시일 가능성이 큽니다. 다음 지침 [https://typefaceapp.com/help/troubleshooting/macos-font-cache] 에 따라 캐시를 지울 수 있습니다. 그래도 해결되지 않으면 ~/Library/Fonts 같은 위치에 있는 손상된 글꼴 파일을 찾아보세요. macOS에서 손상된 글꼴을 쉽게 확인하는 방법은 “서체 관리자(Font Book)” 앱을 열고 모든 글꼴을 선택한 뒤 파일 메뉴에서 “글꼴 유효성 확인”을 선택하는 것입니다.

calibre 전자책 뷰어를 실행하면 검은 화면 또는 흰 화면만 나옵니다?

이것은 뷰어가 렌더링에 사용하는 Qt WebEngine과 시스템 GPU 드라이버 사이의 비호환성 때문입니다. 먼저 GPU 드라이버를 업그레이드해 보세요. 그래도 도움이 안 되면 환경 변수 QTWEBENGINE_CHROMIUM_FLAGS 값을 --disable-gpu 로 설정해 Qt WebEngine의 하드웨어 가속을 끌 수 있습니다. 환경 변수 변경 방법은 환경 변수 를 보세요.

설치 프로그램을 내려받았는데 동작하지 않습니다?

인터넷에서 내려받을 때 파일이 손상되는 경우가 있습니다. 내려받은 calibre 설치 프로그램이 열리지 않으면 다시 내려받아 보세요. 다시 내려받아도 안 되면 다른 위치 [https://github.com/kovidgoyal/calibre/releases/latest] 에서 내려받으세요. 그래도 설치 프로그램이 동작하지 않으면 컴퓨터의 무언가가 실행을 막고 있는 것입니다.

- 백신 프로그램(Microsoft Security Essentials, Kaspersky, Norton, McAfee 등)을 잠시 꺼 보세요. 업그레이드 과정이 중간에서 멈춘다면 가장 가능성이 큰 원인입니다.

- 마찬가지로 설치가 실패하거나 롤백되는데 Microsoft PowerToys가 실행 중이라면 종료하세요.

- 표준이 아닌 위치에 설치했다면 설치 프로그램을 관리자 권한으로 실행해 보세요.

- 컴퓨터를 재부팅하고 Wise registry cleaner [https://www.wisecleaner.com] 같은 레지스트리 클리너를 실행해 보세요.

- 깨끗한 설치를 시도하세요. 즉 calibre를 제거하고 C:Program FilesCalibre2 (또는 이전에 calibre를 설치한 위치)를 삭제한 뒤 calibre를 다시 설치하세요. 제거해도 책이나 설정은 건드리지 않습니다.

- 다른 브라우저로 설치 프로그램을 내려받아 보세요. 예를 들어 Microsoft Edge를 사용 중이라면 Firefox나 Chrome을 사용해 보세요.

- Windows에서 DLL 누락 오류가 나면 임시 폴더 권한이 잘못된 경우가 대부분입니다. Windows 탐색기에서 C:UsersUSERNAMEAppDataLocal 로 가서 Temp 폴더를 오른쪽 클릭하고 속성 -> 보안 탭으로 이동하세요. 사용자 계정이 이 폴더에 대해 전체 제어 권한을 가지고 있는지 확인하세요.

그래도 설치 프로그램을 동작시킬 수 없고 Windows를 사용 중이라면 설치 프로그램이 필요 없는 calibre 포터블 설치본 [https://calibre-ebook.com/ko/download_portable] 을 사용할 수 있습니다(ZIP 파일일 뿐입니다).

내 백신 프로그램이 calibre를 바이러스/트로이 목마라고 주장합니다?

가장 먼저 확인할 것은 calibre를 공식 웹사이트 [https://calibre-ebook.com/ko/download] 에서 내려받고 있는지입니다. 오른쪽 광고가 아니라 왼쪽 다운로드 링크를 클릭하고 있는지 확인하세요. calibre는 매우 인기 있는 프로그램이라 악의적인 사람들이 다운로드를 제공하는 척하는 웹사이트를 만들어 사용자를 속이려 합니다.

공식 다운로드인데도 백신 프로그램이 calibre를 바이러스라고 한다면, 백신 프로그램이 틀린 것입니다. 백신 프로그램은 “수상해 보이는” 코드 패턴 같은 휴리스틱으로 바이러스를 탐지합니다. 일종의 프로파일링과 비슷합니다. calibre는 완전한 오픈 소스 제품입니다. 실제로 소스 코드를 직접 살펴보거나(또는 누군가에게 확인을 맡겨) 바이러스가 아님을 검증할 수 있습니다. 이런 오탐은 당신이 구입한 백신 소프트웨어 회사에 신고하세요. 백신이 calibre의 다운로드/설치를 막는다면 잠시 비활성화하고 calibre를 설치한 뒤 다시 활성화하세요.

calibre는 어떻게 백업하나요?

가장 중요한 백업 대상은 모든 책과 메타데이터가 들어 있는 calibre 라이브러리 폴더입니다. 이것은 calibre를 처음 실행할 때 calibre 라이브러리로 선택한 폴더입니다. 라이브러리 폴더 경로는 기본 도구 모음의 calibre 아이콘을 클릭하면 알 수 있습니다. 이 전체 폴더와 그 안의 모든 파일 및 하위 폴더를 백업해야 합니다.

백업한 라이브러리 폴더를 사용하도록 calibre를 바꾸는 것은 쉽습니다. 도구 모음의 calibre 아이콘을 클릭하고 백업 라이브러리 폴더를 선택하면 됩니다. 백업된 라이브러리 폴더에는 사용자 열, 저장된 검색, 모든 책과 메타데이터가 함께 포함됩니다.

calibre 설정/플러그인까지 백업하려면 설정 폴더도 백업해야 합니다. 이 설정 폴더는 환경설정 → 기타 에서 찾을 수 있습니다. 설정 폴더 복원은 공식 지원 대상은 아니지만 대부분의 경우 동작합니다. 복원하려면 백업 폴더 내용을 현재 설정 폴더로 복사하세요.

구입한 EPUB 책을 calibre와 함께 사용하려면 어떻게 하나요(.acsm 파일은 어떻게 하나요)?

대부분의 유료 EPUB 책에는 DRM 이 걸려 있습니다. 그래서 calibre가 이를 열 수 없습니다. 그래도 calibre를 사용해 이런 책을 저장하고 전자책 리더로 전송하는 것은 가능합니다. 먼저 Windows 컴퓨터에서 Adobe Digital Editions로 리더를 인증해야 합니다. 이 작업을 마치면 calibre로 전송한 EPUB 책이 리더에서 정상 동작합니다. 웹사이트에서 epub 책을 구입하면 “.acsm” 파일을 받게 됩니다. 이 파일은 Adobe Digital Editions로 열어야 하며, 그러면 실제 “.epub” 전자책을 내려받습니다. 전자책 파일은 “My Digital Editions” 폴더에 저장되며, այնտեղ서 calibre에 추가할 수 있습니다.

“권한 거부됨” 오류가 발생합니까?

permission denied 오류는 calibre와는 무관한 여러 이유로 발생할 수 있습니다.

- 쓰기 금지가 활성화된 SD 카드를 사용할 때 permission denied 오류가 발생할 수 있습니다.

- macOS에서 장치를 calibre에 연결할 때 권한 오류가 난다면 시스템 환경설정 > 보안 및 개인정보 보호 > 개인정보 보호 > 파일 및 폴더 에서 해결할 수 있습니다.

- 사용자나 사용한 어떤 프로그램이 해당 파일의 권한을 읽기 전용으로 바꾼 경우입니다.

- 장치에 파일 시스템 오류가 있는 경우, 운영 체제가 읽기 전용 모드에서 파일 시스템을 마운트하거나 특정 파일을 복구 보류 중인 읽기 전용으로 표시합니다.

- 파일 소유자가 사용자 자신이 아닌 다른 사용자로 설정된 경우입니다.

- 사용자의 파일이 다른 프로그램에서 열려 있는 경우입니다.

- 파일이 장치에 있다면 장치 루트에 둘 수 있는 최대 파일 수 256개 제한에 도달했을 수 있습니다. 이런 경우 오류 메시지에 나온 장치/SD 카드를 FAT32 파일시스템으로 다시 포맷하거나 SD 카드/장치 메모리에서 일부 파일을 삭제해야 합니다.

calibre 사용을 다시 시작하기 전에 권한 오류의 근본 원인을 먼저 해결해야 합니다. 오류 메시지를 주의 깊게 읽고 어떤 파일을 가리키는지 확인한 뒤 해당 파일이나 그 파일이 들어 있는 폴더의 권한을 수정하세요.

댓글 메타데이터를 리더기에서도 보이게 할 수 있나요?

대부분의 리더기는 이것을 지원하지 않습니다. 제조사에 불만을 제기하세요. 충분히 많은 사람이 불만을 제기하면 언젠가는 바뀔지도 모릅니다. 그동안은 변환 시 “책 시작 부분에 페이지로 메타데이터 삽입” 옵션을 사용해 댓글을 포함한 메타데이터를 전자책 시작 부분의 “재킷 페이지(Jacket page)”에 넣을 수 있습니다. 이 옵션은 변환 설정의 구조 감지 섹션에 있습니다. 이 기능이 적용되려면 반드시 책을 변환 해야 한다는 점을 기억하세요. 이미 변환이 필요 없는 형식이라면 같은 형식에서 같은 형식으로 변환하면 됩니다.

또 다른 방법은 calibre 라이브러리에 있는 모든 책과 그 메타데이터 목록이 들어 있는 전자책 형식의 카탈로그를 만드는 것입니다. 카탈로그 생성 도구에 접근하려면 변환 버튼을 길게 누르세요. 그리고 미리 답하자면, 아니요, 그 카탈로그가 리더기의 책으로 “직접 링크”되게 할 수는 없습니다.

calibre가 내 HTTP 프록시를 사용하게 하려면 어떻게 하나요?

기본적으로 calibre는 운영체제에 설정된 프록시 설정을 사용합니다. 하지만 이것이 잘못된 경우가 있습니다. 예를 들어 Windows에서 Microsoft Edge를 사용하지 않으면 프록시 설정이 최신이 아닐 수 있습니다. http_proxy 와 https_proxy 환경 변수를 설정해 calibre가 특정 프록시 서버를 사용하도록 할 수 있습니다. 변수 형식은 http://username:password@servername 입니다. 이 변수의 올바른 값은 네트워크 관리자에게 문의하세요. calibre는 SOCKS 프록시가 아니라 HTTP 프록시만 지원합니다. calibre가 현재 사용하는 프록시는 환경설정->기타에서 볼 수 있습니다.

calibre에 어떤 기능을 추가하고 싶습니다. 어떻게 해야 하나요?

선택지는 두 가지입니다:

1. calibre를 직접 수정해 패치를 만든 뒤 검토와 포함을 위해 저에게 보내세요. 개발 [https://calibre-ebook.com/ko/get-involved] 을 보세요.

2. 그 기능을 요청하는 버그를 여세요 [https://calibre-ebook.com/ko/bugs]. 당신은 그 기능이 매우 중요하거나 필수라고 생각할 수 있지만, calibre 개발자들은 동의하지 않을 수 있다는 점을 기억하세요. 다행히 calibre는 오픈 소스이므로 언제나 직접 기능을 구현하거나 누군가에게 구현을 의뢰할 수 있습니다. 또한 calibre에는 포괄적인 플러그인 아키텍처가 있으므로 원하는 기능을 플러그인으로 개발할 수도 있습니다. 직접 플러그인을 작성하여 calibre 기능 확장하기 을 보세요.

왜 calibre에는 자동 업데이트가 없나요?

이유는 많습니다:

- 매주 업데이트할 필요가 없습니다. calibre가 잘 동작한다면 업데이트 알림을 끄고 그대로 사용하면 됩니다. 1년쯤 뒤에 업데이트할지 다시 확인해도 됩니다. 업데이트 알림 자체에 이 알림을 끄는 체크 상자가 있습니다.

- 현재 calibre 다운로드는 한 달에 약 150TB의 대역폭 [https://calibre-ebook.com/dynamic/downloads] 을 사용합니다. 자동 업데이트를 구현하면 이 사용량이 크게 늘어나 매달 수천 달러가 더 들게 되며, 누군가는 그 비용을 부담해야 합니다.

- 지금처럼 웹사이트로 가는 대신 업데이트를 내려받아 실행하는 대화상자를 구현한다고 해도, calibre를 가장 자주 업데이트하는 사용자에게 절약되는 것은 많아야 주당 다섯 번의 클릭 정도입니다. calibre 개발에는 이것보다 훨씬 우선순위가 높은 일이 많습니다.

- 매주 calibre를 내려받는 것이 정말, 정말 싫지만 최신 상태는 유지하고 싶다면 소스에서 직접 실행하는 방식을 권합니다. 이렇게 하면 업데이트가 아주 쉬워집니다. 지침은 여기 에 있습니다.

- calibre 사용자가 만든 서드파티 자동 업데이트 도구가 calibre 포럼 [https://www.mobileread.com/forums/forumdisplay.php?f=238] 에 있습니다.

- 보다 간편한 업데이트 환경을 위해 `calibre Auto-update`라는 타사 플러그인을 `환경설정->플러그인->새 플러그인 가져오기`를 통해 설치할 수 있습니다.

- 또한 Norton이나 Avast 같은 일부 서드파티 업데이트 도구는 사용자 몰래 소프트웨어를 업데이트할 수 있습니다. calibre가 예상치 않게 업데이트되었다면 이런 프로그램이 설치되어 있는지 확인하세요.

calibre는 어떤 라이선스로 배포되나요?

calibre는 GNU General Public License v3(오픈 소스 라이선스)로 배포됩니다. 즉 소스 코드를 함께 제공하는 한 calibre를 자유롭게 재배포할 수 있습니다. 따라서 제품과 함께 calibre를 CD에 넣고 싶다면 calibre 소스 코드도 CD에 함께 넣어야 합니다. 소스 코드는 여기서 내려받을 수 있습니다 [https://download.calibre-ebook.com]. calibre 변환 결과물은 원하는 대로 사용할 수 있습니다. 하지만 calibre의 코드나 라이브러리를 사용하려면 사용자 소프트웨어도 오픈 소스로 공개해야 합니다. 자세한 내용은 GNU GPL v3 [https://www.gnu.org/licenses/gpl.html] 를 보세요.

USB 메모리에서 calibre를 실행하려면 어떻게 하나요?

포터블 버전의 calibre는 여기 [https://calibre-ebook.com/ko/download_portable] 에서 받을 수 있습니다.

내 Linux 서버에서 뉴스 다운로드나 콘텐츠 서버 같은 calibre 기능 일부를 실행하려면 어떻게 하나요?

먼저 Linux 서버에 calibre를 설치해야 합니다. 서버가 최신 Linux 배포판을 사용한다면 calibre 설치에는 문제가 없을 것입니다.

참고
calibre에는 GLIBC >= 2.31 및 libstdc++ >= 6.0.28이 필요합니다. 서버가 더 오래되었다면 이것들을 소스에서 직접 컴파일하거나, GLIBC >= 2.17이 필요한 calibre 3.48, GLIBC >= 2.13이 필요한 2.85.1, 또는 GLIBC >= 2.10만 필요한 calibre 1.48을 사용해야 합니다. 또한 calibre 명령줄 유틸리티는 실행 중인 X 서버가 필요 없지만, 일부는 시스템에 X 서버 라이브러리가 설치되어 있어야 합니다. 이는 다양한 이미지 처리 작업에 사용되는 Qt가 이 라이브러리들에 링크되기 때문입니다. Qt 모듈 관련 ImportError가 난다면 X 라이브러리가 빠져 있을 가능성이 큽니다. 대표적인 후보는 libxcb-cursor0, libxcb-xinerama0, libegl1, libopengl0 입니다.

다음 명령으로 calibre 서버를 실행할 수 있습니다:

/opt/calibre/calibre-server /path/to/the/library/you/want/to/share

다음 명령을 사용하여 뉴스를 다운로드하고 전자책으로 변환할 수 있습니다:

/opt/calibre/ebook-convert "Title of news source.recipe" outputfile.epub

MOBI를 생성하려면 대신 outputfile.mobi 를 사용하고 --output-profile kindle 를 사용하세요.

다음 명령으로 내려받은 뉴스를 이메일로 보낼 수 있습니다:

/opt/calibre/calibre-smtp

정확한 명령줄은 독자에게 숙제로 남겨 두겠습니다.

마지막으로 다음과 같이 내려받은 뉴스를 calibre 라이브러리에 추가할 수 있습니다:

/opt/calibre/calibredb add --with-library /path/to/library outfile.epub

이 명령들과 다른 명령들에 대해 더 배우려면 calibre 사용자 설명서의 명령행 인터페이스 섹션을 읽으세요.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 튜토리얼

튜토리얼

여기에서 XPath 및 템플릿과 같은 캘리버의 더 많은 고급 특정 기능들을 사용하기 위한 튜토리얼을 찾을 수 있습니다.

- 즐겨찾는 뉴스 웹사이트 추가
- 책의 하위 그룹 관리, 예를 들어 “장르”
- XPath 튜토리얼
- calibre 템플릿 언어
- calibre에서 정규 표현식 사용하기

- 직접 플러그인을 작성하여 calibre 기능 확장하기

- 전자책의 수학 조판하기
- AZW3 • EPUB • MOBI 카탈로그 만들기
- 가상 라이브러리

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 튜토리얼 »
- 책의 하위 그룹 관리, 예를 들어 “장르”

책의 하위 그룹 관리, 예를 들어 “장르”

일부 사용자는 하위 폴더와 유사하게 라이브러리의 책을 하위 그룹으로 구성하고 싶어합니다. 가장 흔히 제시되는 이유는 장르 계층 구조를 만드는 것이지만, 다른 이유도 많습니다. 한 사용자는 교과서를 과목과 강좌 번호로 정리하는 방법을 요청했습니다. 다른 사용자는 선물을 주제와 수신자별로 추적하고 싶어했습니다. 이 튜토리얼에서는 이 글의 나머지 부분에서 장르 예시를 사용합니다.

계속하기 전에, 하드 디스크의 폴더에 대해 이야기하는 것이 아님을 유의하세요. 하위 그룹은 파일 폴더가 아닙니다. 책이 어디로 복사되는 것은 아닙니다. 캘리버의 라이브러리 파일 구조에 영향을 주지 않습니다. 대신, 캘리버 라이브러리 내에서 책의 하위 그룹을 구성하고 표시하는 방법을 제시합니다.

- 설정

- 검색

- 제한

- 유용한 템플릿 함수

장르와 같은 하위 그룹에 대해 일반적으로 제시되는 요구 사항은 다음과 같습니다:

- 하위 그룹(예: 장르)은 책의 범주가 아닌 책을 포함(가리켜야) 해야 합니다. 이는 하위 그룹을 캘리버 사용자 범주와 구별하는 것입니다.

- 하나의 책이 여러 하위 그룹(장르)에 속할 수 있습니다. 이는 하위 그룹을 물리적 파일 폴더와 구별하는 것입니다.

- 하위 그룹(장르)은 계층 구조를 형성해야 합니다. 하위 그룹은 하위 그룹을 포함할 수 있습니다.

태그는 처음 두 가지를 제공합니다. 책에 장르를 태그하면 태그 브라우저(또는 검색)를 사용하여 해당 장르의 책을 찾을 수 있으므로 첫 번째를 충족합니다. 많은 책이 같은 태그를 가질 수 있으므로 두 번째를 충족합니다. 문제는 태그가 세 번째 요구 사항을 충족하지 못한다는 것입니다. 계층 구조를 제공하지 않습니다.

캘리버의 계층 기능은 세 번째를 제공합니다 – 장르를 ‘트리’로 볼 수 있는 능력과 장르 또는 하위 장르에서 책을 쉽게 검색할 수 있는 능력입니다. 예를 들어, 장르 구조가 다음과 유사하다고 가정해 보겠습니다:

Genre
. History
.. Japanese
.. Military
.. Roman
. Mysteries
.. English
.. Vampire
. Science Fiction
.. Alternate History
.. Military
.. Space Opera
. Thrillers
.. Crime
.. Horror
etc.

계층 기능을 사용하면 스크린샷과 같이 태그 브라우저에서 이러한 장르를 트리 형태로 볼 수 있습니다. 이 예에서 가장 바깥쪽 수준(장르)은 장르를 포함하는 사용자 정의 열입니다. 하위 장르를 포함하는 장르 옆에는 작은 삼각형이 나타납니다. 해당 삼각형을 클릭하면 항목이 열리고 역사와 공상 과학에서 볼 수 있듯이 하위 장르가 표시됩니다.

장르를 클릭하면 해당 장르 또는 해당 장르의 하위 항목이 있는 모든 책을 검색할 수 있습니다. 예를 들어, 공상 과학을 클릭하면 대체 역사, 군사, 스페이스 오페라 세 가지 하위 장르를 모두 표시합니다. 대체 역사를 클릭하면 군사와 스페이스 오페라에 있는 책은 무시하고 해당 장르의 책만 표시합니다. 물론 하나의 책이 여러 장르를 가질 수 있습니다. 책에 스페이스 오페라와 군사 장르가 모두 있으면 두 장르 중 어느 것을 클릭하든 해당 책이 표시됩니다. 검색에 대해서는 아래에서 자세히 설명합니다.

이미지에서 볼 수 있는 또 다른 것은 군사 장르가 역사 아래 한 번, 공상 과학 아래 한 번, 두 번 나타난다는 것입니다. 장르가 계층 구조에 있으므로 이들은 서로 다른 두 개의 별도 장르입니다. 책은 하나 또는 다른 하나에 속하거나 (이 경우에는 의심스럽지만) 둘 다 속할 수 있습니다. 예를 들어, 윈스턴 처칠의 “제2차 세계 대전” 책은 “역사.군사”에 있을 수 있습니다. 데이비드 위버의 해링턴 시리즈는 “공상 과학.군사”에 있을 수 있고, 마찬가지로 “공상 과학.스페이스 오페라”에도 있을 수 있습니다.

장르가 존재하면, 즉 최소 한 권의 책이 해당 장르를 가지고 있으면, 라이브러리 보기에서 책을 원하는 장르로 끌어다 놓아 쉽게 다른 책에 적용할 수 있습니다. 메타데이터 편집기에서도 장르를 적용할 수 있습니다. 이에 대해서는 아래에서 자세히 설명합니다.

설정

이제 “이 모든 것이 어떻게 설정되었나요?”라는 질문이 있을 수 있습니다. 세 단계가 있습니다: 1) 사용자 정의 열 만들기, 2) 새 열을 계층으로 취급하도록 캘리버에 알리기, 3) 장르 추가하기.

환경설정 -> 사용자 정의 열 추가를 사용하여 일반적인 방법으로 사용자 정의 열을 만듭니다. 이 예에서는 조회 이름으로 “#genre”를 사용하고 열 머리글로 “장르”를 사용합니다. 열 유형이 :guilabel:`태그와 같은 쉼표로 구분된 텍스트, 태그 브라우저에 표시`로 설정되어 있어야 합니다.

그런 다음 캘리버를 다시 시작한 후 해당 열을 계층으로 취급하도록 캘리버에 알려야 합니다. :guilabel:`환경설정 -> 모양 및 느낌 -> 태그 브라우저 -> 계층 및 검색`으로 이동하여 새 장르 열이 계층적 항목을 포함하도록 선택하세요.

이 시점에서는 열에 장르가 없습니다. 마지막 단계가 남아 있습니다: 책에 장르를 적용하는 방법. 장르는 최소 한 권의 책에 나타날 때까지 캘리버에 존재하지 않습니다. 처음으로 장르를 적용하는 방법을 배우려면 책의 메타데이터에서 장르가 어떻게 보이는지 자세히 알아봐야 합니다.

‘항목’의 계층 구조는 마침표로 구분된 구문으로 구성된 항목을 만들어 구성합니다. 장르 예시를 계속하면, 이러한 항목은 “역사.군사”, “미스터리.뱀파이어”, “공상 과학.스페이스 오페라” 등이 됩니다. 따라서 새 장르를 만들려면 해당 장르를 가져야 하는 책을 선택하고 메타데이터를 편집한 다음 만든 열에 새 장르를 입력합니다. 예시를 계속하여, 책에 새 장르 “만화”와 하위 장르 “슈퍼히어로”를 할당하려면 해당 (만화) 책의 ‘메타데이터 편집’을 선택하고 사용자 정의 메타데이터 탭을 선택한 다음 다음 그림과 같이 “만화.슈퍼히어로”를 입력하세요(다른 사용자 정의 열은 무시하세요):

위를 수행한 후 태그 브라우저에서 다음을 볼 수 있습니다:

이제부터 이 새 장르를 책(아마도 만화책)에 적용하려면, 책을 해당 장르로 끌어다 놓거나 위와 동일한 방법으로 메타데이터 편집을 사용하여 책에 추가할 수 있습니다.

참고

계층적 표시는 태그 브라우저가 이름순으로 항목을 정렬하도록 설정된 경우에만 작동합니다. 이것이 기본값이며 태그 브라우저 하단의 구성 버튼을 클릭하여 확인할 수 있습니다.

검색

장르를 검색하는 가장 쉬운 방법은 태그 브라우저를 사용하여 보고 싶은 장르를 클릭하는 것입니다. 하위 항목이 있는 장르를 클릭하면 해당 장르와 모든 하위 장르가 있는 책이 표시됩니다. 그러나 여기서 질문이 생길 수 있습니다. 장르에 하위 항목이 있다고 해서 자체적으로 장르가 아닌 것은 아닙니다. 예를 들어, 책이 “역사” 장르를 가질 수 있지만 “역사.군사”는 아닐 수 있습니다. “역사”만 있는 책을 어떻게 검색하나요?

태그 브라우저 검색 메커니즘은 항목에 하위 항목이 있는지 알고 있습니다. 하위 항목이 있으면 항목을 클릭하면 일반적인 세 가지 대신 다섯 가지 검색을 순환합니다. 첫 번째는 일반적인 녹색 더하기로, 해당 장르만 있는 책을 표시합니다(예: 역사). 두 번째는 이중 더하기(위에 표시됨)로, 해당 장르와 모든 하위 장르가 있는 책을 표시합니다(예: 역사 및 역사.군사). 세 번째는 일반적인 빨간색 빼기로, 해당 정확한 장르가 없는 책을 표시합니다. 네 번째는 이중 빼기로, 해당 장르나 하위 장르가 없는 책을 표시합니다. 다섯 번째는 처음으로 돌아가며, 표시가 없음을 의미합니다.

제한

장르를 검색한 다음 저장된 검색을 만들면 ‘제한’ 상자를 사용하여 해당 장르의 책으로 구성된 가상 라이브러리를 만들 수 있습니다. 이는 해당 장르 내에서 다른 검색을 수행하거나 해당 장르의 책에 대한 메타데이터를 관리/업데이트하려는 경우 유용합니다. 예시를 계속하여, 먼저 태그 브라우저에서 일본 장르를 클릭하여 검색 필드에 검색을 입력하고, 저장된 검색 필드에 역사를 입력한 다음, “검색 저장” 버튼(오른쪽에 있는 흰색 더하기가 있는 녹색 상자)을 눌러 ‘역사.일본’이라는 저장된 검색을 만들 수 있습니다.

저장된 검색을 만든 후 제한으로 사용할 수 있습니다.

유용한 템플릿 함수

디스크에 저장하거나 기기로 보내기와 같은 템플릿에서 장르 정보를 사용하고 싶을 수 있습니다. 그러면 “가장 바깥쪽 장르 이름을 어떻게 가져오나요?”라는 질문이 있을 수 있습니다. 이를 더 쉽게 수행하기 위해 캘리버 템플릿 함수인 subitems가 제공됩니다.

예를 들어, 디스크에 저장하기 템플릿에 가장 바깥쪽 장르 수준을 추가하여 “역사/The Gathering Storm - Churchill, Winston”과 같이 장르 폴더를 만들고 싶다고 가정해 보겠습니다. 이렇게 하려면 계층의 첫 번째 수준을 추출하고 폴더를 만들어야 함을 나타내는 슬래시와 함께 앞에 추가해야 합니다. 아래 템플릿이 이를 수행합니다:

{#genre:subitems(0,1)||/}{title} - {authors}

템플릿과 subitems() 함수에 대한 자세한 내용은 :ref:`템플릿 언어 <templatelangcalibre>`를 참조하세요.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 튜토리얼 »
- XPath 튜토리얼

XPath 튜토리얼

이 튜토리얼에서는 calibre에서 HTML [https://en.wikipedia.org/wiki/HTML] 문서의 임의 부분을 선택하는 데 사용할 수 있는 쿼리 언어인 `XPath <https://en.wikipedia.org/wiki/XPath>`_에 대해 쉽게 소개합니다. XPath는 널리 사용되는 표준이며, 검색하면 많은 정보를 얻을 수 있습니다. 그러나 이 튜토리얼은 비정형 HTML 문서에서 장 머리글을 찾는 것과 같은 전자책 관련 작업에 XPath를 사용하는 데 초점을 맞춥니다.

목차

- 태그 이름으로 선택

- 속성으로 선택

- 태그 내용으로 선택

- 샘플 전자책

- XPath 내장 함수

태그 이름으로 선택

가장 간단한 선택 형태는 이름으로 태그를 선택하는 것입니다. 예를 들어, 문서의 모든 <h2> 태그를 선택하고 싶다고 가정합니다. 이에 대한 XPath 쿼리는 간단합니다:

//h:h2 (Selects all <h2> tags)

접두사 //`는 *문서의 모든 수준에서 검색*을 의미합니다. 이제 ``<a>` 태그 안에 있는 <span> 태그를 검색하고 싶다고 가정합니다. 다음과 같이 할 수 있습니다:

//h:a/h:span (Selects <span> tags inside <a> tags)

문서의 특정 수준에서 태그를 검색하려면 접두사를 변경하세요:

/h:body/h:div/h:p (Selects <p> tags that are children of <div> tags that are
children of the <body> tag)

이것은 sample_ebook`에서 ``<p>A very short e-book to demonstrate the use of XPath.</p>``와만 일치하며 다른 ``<p>` 태그와는 일치하지 않습니다. 위 예시의 h: 접두사는 XHTML 태그와 일치시키기 위해 필요합니다.这是因为 내부적으로 calibre는 모든 콘텐츠를 XHTML로 표현하기 때문입니다. XHTML에서 태그는 *네임스페이스*를 가지며, ``h:``는 HTML 태그의 네임스페이스 접두사입니다.

이제 <h1>``과 ``<h2> 태그를 모두 선택하고 싶다고 가정합니다. 그렇게 하려면 *술어(predicate)*라는 XPath 구문이 필요합니다. :dfn:`술어`는 태그를 선택하는 데 사용되는 간단한 테스트입니다. 테스트는 임의로 강력할 수 있으며 이 튜토리얼이 진행됨에 따라 더 강력한 예시를 보게 될 것입니다. 술어는 테스트 표현식을 대괄호로 감싸서 만듭니다:

//*[name()='h1' or name()='h2']

이 XPath 표현식에는 여러 가지 새로운 기능이 있습니다. 첫 번째는 와일드카드 *``의 사용입니다. *모든 태그와 일치*를 의미합니다. 이제 테스트 표현식 ``name()='h1' or name()='h2'``를 살펴보세요. :term:`name()`은 *내장 함수*의 예시입니다. 태그의 이름으로 평가됩니다. 따라서 이를 사용하여 이름이 `h1`이거나 `h2`인 태그를 선택할 수 있습니다. :term:`name()` 함수는 네임스페이스를 무시하므로 ``h: 접두사가 필요하지 않습니다. XPath에는 여러 유용한 내장 함수가 있습니다. 이 튜토리얼에서 몇 가지를 더 소개하겠습니다.

속성으로 선택

태그의 속성을 기반으로 태그를 선택하려면 술어를 사용해야 합니다:

//*[@style] (Select all tags that have a style attribute)
//*[@class="chapter"] (Select all tags that have class="chapter")
//h:h1[@class="bookTitle"] (Select all h1 tags that have class="bookTitle")

여기서 @ 연산자는 태그의 속성을 참조합니다. XPath 내장 함수 중 일부를 사용하여 속성 값에 대해 더 정교한 일치를 수행할 수 있습니다.

태그 내용으로 선택

XPath를 사용하면 태그가 포함하고 있는 텍스트를 기반으로 태그를 선택할 수도 있습니다. 가장 좋은 방법은 내장 함수 :term:`re:test()`를 통해 *정규 표현식*의 강력함을 활용하는 것입니다:

//h:h2[re:test(., 'chapter|section', 'i')] (Selects <h2> tags that contain the words chapter or
section)

여기서 . 연산자는 태그의 내용을 참조하며, @ 연산자가 태그의 속성을 참조하는 것과 같습니다.

샘플 전자책

<html>
<head>
<title>A very short e-book</title>
<meta name="charset" value="utf-8" />
</head>
<body>
<h1 class="bookTitle">A very short e-book</h1>
<p style="text-align:right">Written by Kovid Goyal</p>
<div class="introduction">
<p>A very short e-book to demonstrate the use of XPath.</p>
</div>

<h2 class="chapter">Chapter One</h2>
<p>This is a truly fascinating chapter.</p>

<h2 class="chapter">Chapter Two</h2>
<p>A worthy continuation of a fine tradition.</p>
</body>
</html>

XPath 내장 함수

name()

현재 태그의 이름입니다.

contains()

``contains(s1, s2)``는 s1이 s2를 포함하면 `true`를 반환합니다.

re:test()

``re:test(src, pattern, flags)``는 문자열 `src`가 정규 표현식 `pattern`과 일치하면 `true`를 반환합니다. 특히 유용한 플래그는 ``i``이며, 대소문자를 구분하지 않고 일치시킵니다. 정규 표현식 구문에 대한 좋은 입문 자료는 `regexp 구문 <https://docs.python.org/library/re.html>`_에서 확인할 수 있습니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 튜토리얼 »
- calibre 템플릿 언어

calibre 템플릿 언어
calibre 템플릿 언어는 파일 경로 지정, 값 서식 지정, 사용자가 지정한 열의 값 계산 같은 작업에 calibre 전반에서 사용되는 calibre 전용 언어이다. 예:

- calibre 라이브러리의 파일을 디스크나 전자책 리더에 저장할 때 폴더 구조와 파일 이름을 지정한다.

- calibre 책 목록에 아이콘과 색상을 추가하는 규칙을 정의한다.

- 다른 열의 데이터를 담는 `가상 열`을 정의한다.

- 고급 라이브러리 검색.

- 고급 메타데이터 검색 및 바꾸기.

이 언어는 `템플릿`이라는 개념을 중심으로 구성되어 있으며, 어떤 책 메타데이터를 사용할지, 그 메타데이터에 대해 어떤 계산을 수행할지, 그리고 어떻게 서식화할지를 지정한다.

기본 템플릿

기본 템플릿은 하나 이상의 template expression``으로 구성된다. ``template expression``은 텍스트와 중괄호(``{}) 안의 이름으로 이루어지며, 처리 중인 책의 해당 메타데이터로 치환된다. 예를 들어 calibre에서 기기로 책을 저장할 때 사용하는 기본 템플릿에는 4개의 ``template expression``이 있다:

{author_sort}/{title}/{title} - {authors}

“The Foundation”의 저자가 “Isaac Asimov”인 책에 대해 이 템플릿은 다음과 같이 된다:

Asimov, Isaac/The Foundation/The Foundation - Isaac Asimov

슬래시는 {} 사이에 있지 않으므로 ``template expression``이 아니다. 이런 텍스트는 있는 위치에 그대로 남는다. 예를 들어 템플릿이 다음과 같다면:

{author_sort} Some Important Text {title}/{title} - {authors}

그러면 “The Foundation”에 대해 템플릿은 다음을 만든다:

Asimov, Isaac Some Important Text The Foundation/The Foundation - Isaac Asimov

``template expression``은 열의 ``lookup name``을 사용하여 calibre에서 사용할 수 있는 모든 메타데이터(사용자가 직접 만든 사용자 정의 열 포함)에 접근할 수 있다. `column`(때로는 `fields`라고도 부름)의 lookup name을 찾으려면 calibre 책 목록의 열 머리글 위에 마우스를 올리면 된다. 사용자 정의 열의 lookup name은 항상 ``#``으로 시작한다. 시리즈 형식 열에는 추가로 ``#lookup name_index``라는 필드가 있으며, 이 값은 해당 책의 시리즈 내 번호이다. 예를 들면:

표준 열 기반 필드 외에 다음도 사용할 수 있다:

- {formats} - 책에 대해 calibre 라이브러리에서 사용 가능한 형식 목록

- {identifiers:select(isbn)} - 책의 ISBN

주어진 책에 대해 해당 필드의 메타데이터가 정의되어 있지 않으면 템플릿의 그 필드는 빈 문자열('')로 치환된다. 예를 들어 다음 템플릿을 보자:

{author_sort}/{series}/{title} {series_index}

아시모프의 책 “Second Foundation”이 “Foundation” 시리즈에 속해 있다면 템플릿은 다음을 만든다:

Asimov, Isaac/Foundation/Second Foundation 3

책에 시리즈가 입력되어 있지 않으면 템플릿은 다음을 만든다:

Asimov, Isaac/Second Foundation

템플릿 처리기는 여러 개의 슬래시와 앞뒤 공백을 자동으로 제거한다.

고급 서식 지정

메타데이터 치환 외에도 템플릿은 추가 텍스트를 조건부로 포함하고, 치환된 데이터의 서식을 제어할 수 있다.

조건부 텍스트 포함

필드가 비어 있지 않을 때만 출력에 텍스트를 표시하고 싶을 때가 있다. 흔한 예는 series``와 ``series_index``로, 아무것도 출력하지 않거나 하이픈으로 구분된 두 값을 출력하고 싶을 수 있다. calibre는 이 경우를 특별한 ``template expression 문법으로 처리한다.

예를 들어 위의 Foundation 예제를 사용해 템플릿이 `Foundation - 3 - Second Foundation`을 만들기를 원한다고 하자. 다음 템플릿은 그 출력을 만든다:

{series} - {series_index} - {title}

하지만 책에 시리즈가 없으면 템플릿은 `- - the title`을 만들며, 이는 보통 원하는 결과가 아니다. 일반적으로는 불필요한 하이픈 없이 제목만 나오기를 원한다. 다음 템플릿 문법을 사용하면 그렇게 할 수 있다:

{field:|prefix_text|suffix_text}

이 ``template expression``은 ``field``의 값이 `XXXX`이면 결과가 `prefix_textXXXXXsuffix_text`가 된다는 뜻이다. ``field``가 비어 있으면(값이 없으면) 접두사와 접미사는 무시되므로 결과는 빈 문자열(아무것도 없음)이다. 접두사와 접미사에는 공백이 들어갈 수 있다.

접두사나 접미사에 서브템플릿(`{ … }`)이나 함수(아래 참조)를 사용하지 마라.

이 문법을 사용하면 위의 시리즈 없음 문제를 다음 템플릿으로 해결할 수 있다:

{series}{series_index:| - | - }{title}

하이픈은 책에 시리즈 번호가 있을 때만 포함되며, 시리즈 번호는 시리즈가 있을 때만 존재한다. Foundation 예제를 계속 사용하면 템플릿은 `Foundation - 1 - Second Foundation`을 만든다.

참고:

- 접두사나 접미사를 사용할 경우 lookup name 뒤에 콜론을 반드시 넣어야 한다.

- | 문자는 둘 다 쓰거나 아예 쓰지 말아야 한다. ``{field:| - }``처럼 하나만 쓰는 것은 허용되지 않는다.

- ``{series:|| - }``처럼 접두사나 접미사 중 하나에 텍스트를 넣지 않아도 된다. 템플릿 ``{title:||}``는 ``{title}``와 같다.

서식 지정

``series_index``를 앞을 0으로 채운 세 자리 숫자로 표시하고 싶다고 하자. 다음과 같이 하면 된다:

{series_index:0>3s} - 앞을 0으로 채운 세 자리

뒤를 0으로 채우려면 다음을 사용한다:

{series_index:0<3s} - 뒤를 0으로 채운 세 자리

시리즈 번호에 1.1 같은 소수 값이 포함되어 있다면 소수점 위치를 맞추고 싶을 수 있다. 예를 들어 1과 2.5를 01.00과 02.50으로 표시해, 사전식 정렬을 사용하는 기기에서도 올바르게 정렬되게 하고 싶을 수 있다. 이 경우 다음을 사용한다:

{series_index:0>5.2f} - 앞을 0으로 채운 두 자리 숫자, 소수점, 그리고 소수점 이하 두 자리로 구성된 총 5자

데이터의 처음 두 글자만 원한다면 다음을 사용한다:

{author_sort:.2} - 저자 정렬 이름의 처음 두 글자만

calibre 템플릿 언어의 서식 지정 상당수는 Python에서 왔다. 이러한 고급 서식 지정 연산의 문법에 대한 자세한 내용은 `Python 문서 <https://docs.python.org/3/library/string.html#formatstrings>`_를 참조하라.

템플릿을 사용해 사용자 정의 열 정의하기

템플릿은 calibre 메타데이터에 없는 정보를 표시하거나, 메타데이터를 calibre의 기본 형식과 다르게 표시하는 데 사용할 수 있다. 예를 들어 calibre가 표시하지 않는 ISBN 필드를 보여주고 싶을 수 있다. 이를 위해 다른 열로부터 만들어진 열 형식(이하 합성 열)의 사용자 정의 열을 만들고, 표시할 텍스트를 생성하는 템플릿을 제공하면 된다. 이 열에는 템플릿 평가 결과가 표시된다. 예를 들면:

복합 열은 템플릿을 포함한 모든 템플릿 옵션을 사용할 수 있습니다.

참고: 합성 열에 표시되는 데이터는 직접 편집할 수 없다. 대신 원본 열을 편집해야 한다. 합성 열을 더블클릭하는 등으로 편집하려 하면 calibre는 실제 데이터가 아니라 템플릿 편집기를 연다.

템플릿과 플러그보드

플러그보드는 기기로 보내기와 디스크에 저장 작업 중 책에 기록되는 메타데이터를 변경하는 데 사용한다. 플러그보드를 사용하면 책 메타데이터에 쓸 데이터를 제공하는 템플릿을 지정할 수 있다. 수정 가능한 필드는 authors, author_sort, language, publisher, tags, title, title_sort이다. 이 기능은 정렬이나 표시 문제를 해결하기 위해 기기의 책에 다른 메타데이터를 쓰고 싶은 사용자에게 유용하다.

플러그보드를 만들 때는 해당 플러그보드가 사용될 형식과 장치를 지정한다. 형식을 저장할 때(기기로 보내는 것이 아니라) 사용하는 특수 장치로 ``save_to_disk``가 제공된다. 형식과 장치를 선택한 후에는 변경할 메타데이터 필드를 선택하고, 새 값을 제공할 템플릿을 지정한다. 이 템플릿들은 대상 필드에 `연결`되므로 `plugboards`라는 이름이 붙었다. 물론 이러한 템플릿에서 합성 열을 사용할 수도 있다.

플러그보드는 매우 유연하며 Single Function Mode, Template Program Mode, General Program Mode, Python Template mode로 작성할 수 있다.

플러그보드가 적용될 수 있을 때(콘텐츠 서버, 디스크에 저장, 기기로 보내기) calibre는 정의된 플러그보드를 검색하여 해당 형식과 장치에 맞는 올바른 플러그보드를 선택한다. 예를 들어 EPUB 책을 ANDROID 장치로 보낼 때 적절한 플러그보드를 찾기 위해 calibre는 다음 순서로 플러그보드를 검색한다:

- 형식과 장치가 정확히 일치하는 플러그보드. 예: EPUB 및 ANDROID

- 형식은 정확히 일치하고 장치는 특수 선택인 any device``인 플러그보드. 예: ``EPUB 및 any device

- 특수 선택인 any format``과 장치의 정확한 일치가 있는 플러그보드. 예: ``any format 및 ANDROID

- any format 및 ``any device``가 지정된 플러그보드

tags 필드와 authors 필드는 둘 다 여러 항목을 담을 수 있기 때문에 특별하게 처리된다. 한 책에는 여러 태그와 여러 저자가 있을 수 있다. 이 두 필드 중 하나를 변경하도록 지정하면 템플릿 결과를 검사하여 항목이 둘 이상인지 확인한다. 태그의 경우 calibre는 쉼표가 있는 곳에서 결과를 분리한다. 예를 들어 템플릿이 Thriller, Horror``를 만들면 결과는 ``Thriller``와 ``Horror 두 개의 태그가 된다. 태그에 쉼표를 넣는 방법은 없다.

저자도 같은 방식으로 처리되지만, 구분 문자로 쉼표 대신 &`(앰퍼샌드)를 사용한다. 예를 들어 템플릿이 ``Blogs, Joe&Posts, Susan``을 만들면 책에는 ``Blogs, Joe``와 ``Posts, Susan` 두 명의 저자가 들어간다. 템플릿이 ``Blogs, Joe;Posts, Susan``을 만들면 책에는 다소 이상한 이름을 가진 한 명의 저자만 들어간다.

플러그보드는 디스크에 저장하거나 장치에 쓸 때 책에 기록되는 메타데이터에 영향을 준다. 하지만 ``디스크에 저장``과 ``기기로 보내기``가 파일 이름을 만들 때 사용하는 메타데이터에는 영향을 주지 않는다. 파일 이름은 대신 해당 환경설정 창에 입력된 템플릿을 사용해 구성된다.

템플릿에서 함수 사용하기 - Single Function Mode

일반적으로 제목 형식인 필드 값을 대문자로 표시하고 싶다고 하자. 이 경우 template functions`를 사용할 수 있다. 예를 들어 제목을 대문자로 표시하려면 ``{title:uppercase()}``처럼 ``uppercase` 함수를 사용한다. 제목 형식으로 표시하려면 ``{title:titlecase()}``를 사용한다.

함수는 템플릿의 형식 부분, 즉 : 뒤이자 첫 번째 | 앞(접두사/접미사가 없으면 닫는 } 앞)에 들어간다. 형식과 함수 참조를 둘 다 사용할 경우 함수는 두 번째 : 뒤에 온다. 함수는 템플릿에서 지정한 열의 값을 적절히 수정하여 반환한다.

함수 사용 문법은 다음 중 하나이다:

{lookup_name:function(arguments)}
{lookup_name:format:function(arguments)}
{lookup_name:function(arguments)|prefix|suffix}
{lookup_name:format:function(arguments)|prefix|suffix}

함수 이름 뒤에는 항상 여는 괄호와 닫는 괄호가 와야 한다. 일부 함수는 추가 값(인수)을 요구하며, 이 값들은 괄호 안에 넣는다. 인수는 쉼표로 구분한다. 문자 그대로의 쉼표(인수 구분자가 아닌 쉼표)는 백슬래시(\)를 앞에 붙여야 한다. 마지막 인수(또는 유일한 인수)에는 문자 그대로의 닫는 괄호를 넣을 수 없다.

함수는 형식 지정과 접두사/접미사보다 먼저 평가된다. 형식과 함수를 함께 사용하는 예는 아래를 참조하라.

중요: 프로그래밍 경험이 있다면 `Single Function Mode`의 문법이 예상과 다르다는 점에 주의하라. 문자열은 따옴표로 감싸지지 않으며 공백이 중요하다. 모든 인수는 상수로 간주되며 식은 없다.

함수 인수로 서브템플릿(`{ … }`)을 사용하지 마라. 대신 :ref:`Template Program Mode <template_mode>`와 :ref:`General Program Mode <general_mode>`를 사용하라.

Single Function Mode에서 함수 호출 시 참고 사항:

- Single Function Mode에서 함수를 사용할 때 첫 번째 매개변수 value``는 템플릿에 지정된 필드의 내용으로 자동 대체된다. 예를 들어 템플릿 ``{title:capitalize()}``를 처리할 때 ``title 필드의 내용이 capitalize 함수의 value 매개변수로 전달된다.

- 함수 문서에서 [something]* 표기는 something``이 0회 이상 반복될 수 있음을 의미한다. ``[something]+ 표기는 ``something``이 1회 이상 반복됨을 의미한다(최소 한 번은 있어야 함).

- 일부 함수는 정규식을 사용한다. 템플릿 언어에서 정규식 일치는 대소문자를 구분하지 않는다.

함수는 template_functions_reference`에 문서화되어 있다. 문서는 함수에 필요한 인수와 함수가 수행하는 일을 알려 준다. 예를 들어 다음은 :ref:`ff_ifempty 함수의 문서이다.

- ifempty(value, text_if_empty) – value 가 비어 있지 않으면 그 값을 반환하고, 비어 있으면 text_if_empty 를 반환합니다.

이 함수는 value``와 ``text_if_empty 두 인수를 요구한다. 하지만 지금은 Single Function Mode를 사용하고 있으므로 value 인수는 생략하고 ``text_if_empty``만 전달한다. 예를 들어 이 템플릿은:

{tags:ifempty(No tags on this book)}

책에 태그가 있으면 태그를 표시한다. 태그가 없으면 `이 책에는 태그가 없습니다`를 표시한다.

다음 함수들은 첫 번째 매개변수가 ``value``이므로 Single Function Mode에서 사용할 수 있다.

capitalize(value) – value 의 첫 글자는 대문자로, 나머지는 소문자로 바꿔 반환합니다.

ceiling(value) – value 이상인 가장 작은 정수를 반환합니다.

cmp(value, y, lt, eq, gt) – value 와 y 를 둘 다 숫자로 변환한 뒤 비교합니다.

contains(value, pattern, text_if_match, text_if_not_match) – 값이 정규식 pattern 과 일치하는지 검사합니다.

date_arithmetic(value, calc_spec, fmt) – calc_spec 을 사용해 value 로부터 새 날짜를 계산합니다.

encode_for_url(value, use_plus) – use_plus 설정에 따라 URL 에 사용할 수 있게 value 를 인코딩해 반환합니다. 값은 먼저 URL 인코딩되며, 이어서 use_plus 가 0 이면 공백을 '+' 로, 아니면 %20 으로 바꿉니다.

floor(value) – value 이하인 가장 큰 정수를 반환합니다.

format_date(value, format_string) – 날짜 문자열이어야 하는 value 를 format_string 으로 형식화해 문자열로 반환합니다.

format_duration(value, template, [largest_unit]) – 초 단위 숫자인 value 를 주, 일, 시, 분, 초를 보여 주는 문자열로 형식화합니다. 값이 실수이면 가장 가까운 정수로 반올림합니다.

format_number(value, template) – value 를 숫자로 해석하고 {0:5.2f}, {0:,d}, ${0:5,.2f} 같은 Python 서식 템플릿을 사용해 형식화합니다.

fractional_part(value) – 값의 소수점 이하 부분을 반환합니다.

human_readable(value) – value 를 숫자로 간주하고 KB, MB, GB 등의 형식으로 표현한 문자열을 반환합니다.

ifempty(value, text_if_empty) – value 가 비어 있지 않으면 그 값을 반환하고, 비어 있으면 text_if_empty 를 반환합니다.

language_strings(value, localize) – value 로 전달된 언어 코드의 언어 이름을 반환합니다(이름과 코드는 여기 참조 [https://www.loc.gov/standards/iso639-2/php/code_list.php]).

list_contains(value, separator, [ pattern, found_val, ]* not_found_val) – value 를 separator 로 구분된 항목 목록으로 해석하고 각 항목을 정규식 pattern 과 비교합니다.

list_count(value, separator) – 값을 separator 로 구분된 항목 목록으로 해석하고 그 개수를 반환합니다.

list_count_matching(value, pattern, separator) – value 를 separator 로 구분된 항목 목록으로 해석하고 정규식 pattern 과 일치하는 항목 수를 반환합니다.

list_item(value, index, separator) – value 를 separator 로 구분된 항목 목록으로 해석하여 index 번째 항목을 반환합니다.

list_sort(value, direction, separator) – 대소문자를 구분하지 않는 사전식 정렬로 value 를 정렬해 반환합니다.

lookup(value, [ pattern, key, ]* else_key) – 패턴들을 순서대로 value 와 대조합니다.

lowercase(value) – ``value``를 소문자로 반환합니다.

mod(value, y) – value / y 의 나머지에 대해 floor 를 적용한 값을 반환합니다.

rating_to_stars(value, use_half_stars) – value 를 별(★) 문자 문자열로 반환합니다.

re(value, pattern, replacement) – 정규식을 적용한 뒤의 value 를 반환합니다.

re_group(value, pattern [, template_for_group]*) – 정규식 pattern 을 value 에 적용해 만들어진 문자열을 반환하고

round(value) – value 에 가장 가까운 정수를 반환합니다.

select(value, key) – value 를 각 항목이 id:id_value 형식인 콤마 구분 목록( calibre identifier 형식 )으로 해석합니다.

shorten(value, left_chars, middle_text, right_chars) – value 의 축약형을 반환합니다.

str_in_list(value, separator, [ string, found_val, ]+ not_found_val) – value 를 separator 로 구분된 항목 목록으로 해석한 뒤

subitems(value, start_index, end_index) – 장르처럼 계층 구조를 가진 태그형 항목 목록을 분해하는 함수입니다.

sublist(value, start_index, end_index, separator) – value 를 separator 로 구분된 항목 목록으로 해석하고, start_index 부터 end_index 까지의 항목으로 이루어진 새 목록을 반환합니다.

substr(value, start, end) – value 의 start 번째 문자부터 end 번째 문자까지를 반환합니다.

swap_around_articles(value, separator) – value 의 관사를 끝으로 옮겨 세미콜론으로 구분해 반환합니다.

swap_around_comma(value) – B, A 형식의 value 를 받아 A B 를 반환합니다.

switch(value, [patternN, valueN,]+ else_value) – 각 patternN, valueN 쌍에 대해 value 가 정규식 patternN 과 일치하는지 검사하고

test(value, text_if_not_empty, text_if_empty) – 값이 비어 있지 않으면 text_if_not_empty 를, 비어 있으면 text_if_empty 를 반환합니다.

titlecase(value) – ``value``를 제목 형식 대소문자로 반환합니다.

transliterate(value) – value 의 단어 소리를 근사해 라틴 문자 알파벳 문자열로 반환합니다.

uppercase(value) – ``value``를 대문자로 반환합니다.

같은 템플릿에서 함수와 서식 함께 사용하기

정수 사용자 정의 열 #myint``를 ``003``처럼 앞을 0으로 채워 표시하고 싶다고 하자. 한 가지 방법은 ``0>3s 형식을 사용하는 것이다. 하지만 기본적으로 숫자(정수 또는 부동소수점)가 0이면 값은 빈 문자열로 표시되므로, 0 값은 000``이 아니라 빈 문자열이 된다. ``000``을 보고 싶다면 형식 문자열과 ``ifempty 함수를 함께 사용해 빈 값을 다시 0으로 바꾸면 된다. 템플릿은 다음과 같다:

{#myint:0>3s:ifempty(0)}

접두사와 접미사도 함께 사용할 수 있다. 숫자를 [003] 또는 ``[000]``처럼 보이게 하려면 다음 템플릿을 사용한다:

{#myint:0>3s:ifempty(0)|[|]}

General Program Mode

General Program Mode`(`GPM)는 `template expressions`를 `template language`로 작성한 프로그램으로 대체한다. 이 언어의 문법은 다음과 같이 정의된다:

program ::= 'program:' expression_list expression_list ::= top_expression [ ';' top_expression ]* top_expression ::= or_expression or_expression ::= and_expression [ '||' and_expression ]* and_expression ::= not_expression [ '&&' not_expression ]* not_expression ::= [ '!' not_expression ]* | concatenate_expr concatenate_expr::= compare_expr [ '&' compare_expr ]* compare_expr ::= add_sub_expr [ compare_op add_sub_expr ]
compare_op ::= '==' | '!=' | '>=' | '>' | '<=' | '<' |
'in' | 'inlist' | 'inlist_field' |
'==#' | '!=#' | '>=#' | '>#' | '<=#' | '<#'
add_sub_expr ::= times_div_expr [ add_sub_op times_div_expr ]* add_sub_op ::= '+' | '-'
times_div_expr ::= unary_op_expr [ times_div_op unary_op_expr ]* times_div_op ::= '*' | '/'
unary_op_expr ::= [ add_sub_op unary_op_expr ]* | expression expression ::= identifier | constant | function | assignment | field_reference | if_expr | for_expr | break_expr | continue_expr | return_stmt
'(' expression_list ')' | function_def field_reference ::= '$' [ '$' ] [ '#' ] identifier identifier ::= id_start [ id_rest ]* id_start ::= letter | underscore id_rest ::= id_start | digit constant ::= " string " | ' string ' | number function ::= identifier '(' expression_list [ ',' expression_list ]* ')'
function_def ::= 'def' identifier '(' top_expression [ ',' top_expression ]* ')' ':'
expression_list 'fed'
assignment ::= identifier '=' top_expression if_expr ::= 'if' condition 'then' expression_list
[ elif_expr ] [ 'else' expression_list ] 'fi'
condition ::= top_expression elif_expr ::= 'elif' condition 'then' expression_list elif_expr | ''
for_expr ::= for_list | for_range for_list ::= 'for' identifier 'in' list_expr
[ 'separator' separator_expr ] ':' expression_list 'rof'
for_range ::= 'for' identifier 'in' range_expr ':' expression_list 'rof'
range_expr ::= 'range' '(' [ start_expr ',' ] stop_expr
[ ',' step_expr [ ',' limit_expr ] ] ')'
with_expr ::= 'with' top_expression ':' expression_list 'htiw'
list_expr ::= top_expression break_expr ::= 'break'
continue_expr ::= 'continue'
return_stmt ::= 'return' top_expression separator_expr ::= top_expression start_expr ::= top_expression stop_expr ::= top_expression step_expr ::= top_expression limit_expr ::= top_expression
참고:

- ``top_expression``은 항상 값을 가진다. ``expression_list``의 값은 목록에서 마지막 ``top_expression``의 값이다. 예를 들어 식 목록 ``1;2;’foobar’;3``의 값은 ``3``이다.

- 논리 문맥에서 비어 있지 않은 값은 모두 ``True``이다

- 논리 문맥에서 빈 값은 ``False``이다

- 문자열과 숫자는 서로 바꾸어 사용할 수 있다. 예를 들어 ``10``과 ``’10’``은 같은 것이다.

- 주석은 빈칸이나 탭이 앞에 있을 수 있는, # 문자로 시작하는 줄이다.

연산자 우선순위

연산자 우선순위(평가 순서)는 가장 높은 것(가장 먼저 평가)부터 가장 낮은 것(가장 나중에 평가)까지 다음과 같다:

- 함수 호출, 상수, 괄호로 묶인 식, 문장 식, 할당 식, 필드 참조.

- 단항 플러스(+)와 마이너스(-). 이 연산자들은 오른쪽에서 왼쪽으로 평가된다.

이 연산자들과 다른 모든 산술 연산자는 식의 결과에 소수 부분이 없으면 정수를 반환한다. 예를 들어 식이 ``3.0``을 반환하면 ``3``으로 바뀐다.

- 곱하기(*)와 나누기(/). 이 연산자들은 결합법칙이 적용되며 왼쪽에서 오른쪽으로 평가된다. 평가 순서를 바꾸고 싶다면 괄호를 사용하라.

- 더하기(+)와 빼기(-). 이 연산자들은 결합법칙이 적용되며 왼쪽에서 오른쪽으로 평가된다.

- 숫자 및 문자열 비교. 이 연산자들은 비교가 성공하면 '1'``을, 그렇지 않으면 빈 문자열(‘’``)을 반환한다. 비교는 결합적이지 않으므로 ``a < b < c``는 문법 오류이다.

- 문자열 연결(&). & 연산자는 왼쪽 식과 오른쪽 식을 이어 붙인 문자열을 반환한다. 예: ``’aaa’ & ‘bbb’``는 ``’aaabbb’``를 반환한다. 이 연산자는 결합법칙이 적용되며 왼쪽에서 오른쪽으로 평가된다.

- 단항 논리 부정(!). 이 연산자는 식이 False(빈 문자열로 평가됨)면 ``’1’``을, 그렇지 않으면 ``’’``을 반환한다.

- 논리곱(&&). 이 연산자는 왼쪽 식과 오른쪽 식이 모두 True이면 ‘1’을, 둘 중 하나라도 False이면 빈 문자열 ``’’``을 반환한다. 결합법칙이 적용되며 왼쪽에서 오른쪽으로 평가되고, `단락 평가 <https://chortle.ccsu.edu/java5/Notes/chap40/ch40_2.html>`_를 수행한다.

- 논리합(||). 이 연산자는 왼쪽 식이나 오른쪽 식 중 하나라도 True이면 ``’1’``을, 둘 다 False이면 ``’’``을 반환한다. 결합법칙이 적용되며 왼쪽에서 오른쪽으로 평가되고, 단락 평가 <https://chortle.ccsu.edu/java5/Notes/chap40/ch40_2.html>`_를 수행한다. 이는 `포괄적 or`이므로 왼쪽과 오른쪽 식이 모두 True여도 `’1’``을 반환한다.

필드 참조
field_reference``는 ``$ 또는 $$ 뒤에 오는 lookup name으로 지정된 메타데이터 필드의 값을 평가한다. $ 사용은 field 함수를 사용하는 것과 같다. $$ 사용은 raw_field 함수를 사용하는 것과 같다. 예:

* $authors ==> field('authors')
* $#genre ==> field('#genre')
* $$pubdate ==> raw_field('pubdate')
* $$#my_int ==> raw_field('#my_int')

If 식

If 식은 먼저 condition``을 평가한다. ``condition``이 True(비어 있지 않은 값)이면 ``then 절의 expression_list``를 평가한다. False이면 ``elif 또는 else 절이 있을 경우 그 안의 expression_list``를 평가한다. ``elif``와 ``else 부분은 선택 사항이다. if, then, elif, else, ``fi``는 예약어이므로 식별자 이름으로 사용할 수 없다. 줄바꿈과 공백은 의미가 통하는 위치라면 어디든 넣을 수 있다. 문법은 다음과 같다:

예:

* program: if field('series') then 'yes' else 'no' fi
* program:
if field('series') then
a = 'yes';
b = 'no'
else
a = 'no';
b = 'yes'
fi;
strcat(a, '-', b)

중첩된 if 예제:

program:
if field('series') then
if check_yes_no(field('#mybool'), '', '', '1') then
'yes'
else
'no'
fi
else
'no series'
fi

위에서 말했듯이 ``if``는 값을 생성한다. 즉, 다음 예들은 모두 동일하다:

* program: if field('series') then 'foo' else 'bar' fi
* program: if field('series') then a = 'foo' else a = 'bar' fi; a
* program: a = if field('series') then 'foo' else 'bar' fi; a

예를 들어 다음 프로그램은 책에 시리즈가 있으면 series 열의 값을, 없으면 title 열의 값을 반환한다:

program: field(if field('series') then 'series' else 'title' fi)

For 식
for 식은 값 목록을 순회하면서 한 번에 하나씩 처리한다. list_expression``은 메타데이터 필드 ``lookup name``(예: ``tags 또는 #genre) 또는 값 목록으로 평가되어야 한다. :ref:`ff_range`는 숫자 목록을 생성한다. 결과가 유효한 ``lookup name``이면 해당 필드의 값을 가져오고, 그 필드 형식에 지정된 구분자를 사용한다. 결과가 유효한 ``lookup name``이 아니면 값 목록으로 간주한다. 목록은 쉼표로 분리되어 있다고 가정한다. 문법은 다음과 같다:

예: 이 템플릿은 Genre(#genre)의 각 값에서 첫 번째 계층 이름을 제거하여 새 이름 목록을 만든다:

program:
new_tags = '';
for i in '#genre':
j = re(i, '^.*?\.(.*)$', '\1');
new_tags = list_union(new_tags, j, ',')
rof;
new_tags

원래 Genre가 `History.Military, Science Fiction.Alternate History, ReadMe`라면 템플릿은 `Military, Alternate History, ReadMe`를 반환한다. 이 템플릿을 calibre의 :guilabel:`메타데이터 일괄 편집 -> 검색 및 바꾸기`에서 :guilabel:`검색 대상`을 ``template``으로 설정하여 사용하면, 계층 구조의 첫 번째 수준을 제거하고 그 결과 값을 Genre에 할당할 수 있다.

참고: 이 경우 템플릿의 마지막 줄인 new_tags``는 엄밀히 말해 필요하지 않다. ``for``는 식 목록에서 마지막 top_expression의 값을 반환하기 때문이다. 할당의 값은 그 식의 값이므로 ``for 문의 값은 ``new_tags``에 할당된 값이다.

with 식
with 식은:

1. ``top_expression``을 평가해 얻은 calibre 책 ID(정수)를 가진 책으로 현재 책을 바꾼다.

2. ``expression_list``를 실행한다.

3. 그런 다음 현재 책을 원래 값으로 되돌린다.

with 식은 평가된 expression_list``의 마지막 ``top_expression 결과를 반환하며, 식 목록이 평가되지 않았다면 빈 문자열을 반환한다.

예를 들어 다음 템플릿은 GUI에서 선택된 각 책의 제목 목록을 반환한다:

program:
res = '';
ids = selected_books();
for id in ids:
with id:
res = (if res then res & ', ' fi) & $title
htiw
rof;
res

Return 문

``expression``의 값을 반환한다. 함수 안에서 실행되면 그 값을 호출자에게 반환한다. 가장 바깥 문맥(템플릿)에서 실행되면 템플릿의 값을 해당 식의 값으로 설정하고 템플릿 실행을 종료한다.

함수 정의

템플릿에 반복되는 코드가 있다면 그 코드를 지역 함수로 만들 수 있다. def 키워드가 정의를 시작한다. 그 뒤에 함수 이름, 인수 목록, 그리고 함수 본문의 코드가 온다. 함수 정의는 fed 키워드로 끝난다.

인수는 위치 기반이다. 함수를 호출하면 전달된 인수는 정의된 매개변수와 왼쪽에서 오른쪽으로 대응되며, 해당 인수의 값이 매개변수에 할당된다. 정의된 매개변수보다 많은 인수를 전달하면 오류이다. 매개변수는 ``a = 25``처럼 기본값을 가질 수 있다. 그 매개변수에 인수가 전달되지 않으면 기본값이 사용되고, 그렇지 않으면 빈 문자열이 설정된다.

지역 함수 안에서는 return 문을 사용할 수 있다.

함수는 사용하기 전에 정의되어 있어야 한다.

예: 이 템플릿은 일 수를 기준으로 대략적인 기간을 연, 월, 일 단위로 계산한다. 함수 to_plural()``은 계산된 값을 서식화한다. 이 예에서는 ``& 연산자도 사용한다:

program:
days = 2112;
years = floor(days/360);
months = floor(mod(days, 360)/30);
days = days - ((years*360) + (months * 30));

def to_plural(v, str):
if v == 0 then return '' fi;
return v & ' ' & (if v == 1 then str else str & 's' fi) & ' '
fed;

to_plural(years, 'year') & to_plural(months, 'month') & to_plural(days,'day')

관계 연산자

관계 연산자는 비교가 참이면 '1'``을, 그렇지 않으면 빈 문자열(‘’``)을 반환한다.

관계 연산자는 문자열 비교와 숫자 비교 두 종류가 있다.

문자열 비교는 사전식 순서를 사용하며 대소문자를 구분하지 않는다. 지원되는 문자열 비교 연산자는 ==, !=, <, <=, >, >=, in, inlist, inlist_field``이다. ``in, inlist, inlist_field 연산자에서는 왼쪽 식의 결과를 정규식 패턴으로 해석한다. 왼쪽 정규식이 오른쪽 식의 값과 일치하면 참이다. 정규식은 대소문자를 구분하지 않는다.

inlist 연산자는 왼쪽 정규식이 오른쪽 목록의 항목 중 하나라도 일치하면 참이다. 이 목록의 항목은 쉼표로 구분된다. inlist_field 연산자는 오른쪽 식이 가리키는 필드(열)의 항목 중 하나라도 왼쪽 정규식과 일치하면 참이며, 필드에 정의된 구분자를 사용한다. 참고로 ``inlist_field``는 오른쪽 식이 필드 이름으로 평가되어야 하지만, ``inlist``는 단순 문자열 목록을 기대한다.

숫자 비교 연산자는 ==#, !=#, <#, <=#, >#, ``>=#``이다. 왼쪽과 오른쪽 식은 숫자 값으로 평가되어야 한다. 단, 두 가지 예외가 있다. 문자열 값 “None”(정의되지 않은 필드)과 빈 문자열은 모두 값 0으로 평가된다.

예:

program: field('series') == 'foo' 는 책의 시리즈가 foo`이면 `’1’``을, 그렇지 않으면 ``’’``을 반환한다.

program: 'f.o' in field('series') 는 책의 시리즈가 정규식 ``f.o``(예: foo, Off Onyx 등)와 일치하면 ``’1’``을, 그렇지 않으면 ``’’``을 반환한다.

program: 'science' inlist $#genre 는 책의 장르 값 중 하나라도 정규식 ``science``(예: Science, History of Science, Science Fiction 등)와 일치하면 ``’1’``을, 그렇지 않으면 ``’’``을 반환한다.

program: '^science$' inlist $#genre 는 책의 장르 중 하나라도 정규식 ``^science$``와 정확히 일치하면(예: Science) ``’1’``을, 그렇지 않으면 ``’’``을 반환한다. `History of Science`와 `Science Fiction`은 일치하지 않는다.

program: 'asimov' inlist $authors 는 저자 중 하나라도 정규식 ``asimov``와 일치하면(예: Asimov, Isaac 또는 Isaac Asimov) ``’1’``을, 그렇지 않으면 ``’’``을 반환한다.

program: 'asimov' inlist_field 'authors' 는 저자 중 하나라도 정규식 ``asimov``와 일치하면(예: Asimov, Isaac 또는 Isaac Asimov) ``’1’``을, 그렇지 않으면 ``’’``을 반환한다.

program: 'asimov$' inlist_field 'authors' 는 저자 중 하나라도 정규식 asimov$``와 일치하면(예: `Isaac Asimov`) ``'1'``을, 그렇지 않으면 ``''``을 반환한다. 정규식의 ``$ 앵커 때문에 `Asimov, Isaac`에는 일치하지 않는다.

program: if field('series') != 'foo' then 'bar' else 'mumble' fi 는 책의 시리즈가 foo`가 아니면 `’bar’``를 반환한다. 그렇지 않으면 ``’mumble’``을 반환한다.

program: if field('series') == 'foo' || field('series') == '1632' then 'yes' else 'no' fi 는 시리즈가 foo 또는 1632`이면 `’yes’``를, 그렇지 않으면 ``’no’``를 반환한다.

program: if '^(foo|1632)$' in field('series') then 'yes' else 'no' fi 는 시리즈가 foo 또는 1632`이면 `’yes’``를, 그렇지 않으면 ``’no’``를 반환한다.

program: if 11 > 2 then 'yes' else 'no' fi 는 > 연산자가 사전식 비교를 수행하므로 ``’no’``를 반환한다.

program: if 11 ># 2 then 'yes' else 'no' fi 는 ># 연산자가 숫자 비교를 수행하므로 ``’yes’``를 반환한다.

General Program Mode의 함수

`template language`에 내장된 함수 목록은 :ref:`template_functions_reference`를 참조하라.

참고:

- :ref:`Single Function Mode <single_mode>`와 달리 General Program Mode에서는 첫 번째 매개변수 ``value``를 반드시 지정해야 한다.

- 모든 매개변수는 expression_list이다(위 문법 참조).

템플릿 식에서 더 복잡한 프로그램 사용하기 - Template Program Mode

Template Program Mode`(`TPM)는 General Program Mode <general_mode>`와 :ref:`Single Function Mode <single_mode>`를 결합한 형태이다. `TPM`은 다른 메타데이터 필드를 참조하고, 중첩 함수를 사용하고, 변수를 수정하고, 산술 연산을 할 수 있다는 점에서 Single Function Mode와 다르다. `General Program Mode`와는 템플릿이 ``{` 와 } 사이에 들어가며 ``program:``이라는 단어로 시작하지 않는다는 점이 다르다. 템플릿의 프로그램 부분은 다음과 같이 구문 분석된다:

예: 책에 시리즈가 있으면 시리즈를 표시하고, 없으면 사용자 정의 필드 #genre 값을 표시하는 템플릿을 원한다고 하자. :ref:`Single Function Mode <single_mode>`에서는 템플릿 식 안에서 다른 메타데이터 필드를 참조할 수 없기 때문에 이를 할 수 없다. `TPM`에서는 가능하며, 다음 식이 그 예이다:

{series:'ifempty($, $#genre)'}

이 예는 여러 가지를 보여 준다:

- 식이 ``:’``로 시작하고 ``’}``로 끝나면 `TPM`이 사용된다. 그 외의 경우는 :ref:`Single Function Mode <single_mode>`로 간주된다.

템플릿에 접두사와 접미사가 있으면 식은 접두사의 구분자인 ``|``가 붙은 ``’|``로 끝난다. 예:

{series:'ifempty($, $#genre)'|prefix | suffix}

- 함수에는 모든 인수를 제공해야 한다. 예를 들어 표준 내장 함수에는 초기 매개변수 ``value``를 전달해야 한다.

- 변수 $``는 ``value 인수로 사용할 수 있으며, 여기서는 템플릿에 지정된 필드 ``series``의 값을 뜻한다.

- 식 내부에서는 공백이 무시되므로 어디에든 사용할 수 있다.

- 상수 문자열은 짝이 맞는 따옴표, 즉 ' 또는 ``”``로 감싼다.

TPM`에서 문자열 리터럴 안에 ``{` 와 } 문자를 사용하면 템플릿 처리기가 이를 문자 대신 템플릿 식 경계로 해석하려 하므로 오류나 예상치 못한 결과가 날 수 있다. 경우에 따라 {``를 ``[[``로, ``}``를 `]]`로 바꿀 수 있지만 항상 되는 것은 아니다. 조언: 프로그램에 ``{ 와 } 문자가 포함되어 있다면 `General Program Mode`를 사용하라.

Python Template Mode

Python Template Mode(PTM)는 네이티브 Python과 `calibre API <https://manual.calibre-ebook.com/develop.html#api-documentation-for-various-parts-of-calibre>`_를 사용해 템플릿을 작성할 수 있게 해 준다. 가장 유용한 것은 데이터베이스 API이며, 자세한 설명은 이 매뉴얼 범위를 벗어난다. PTM 템플릿은 더 빠르고 더 복잡한 작업을 수행할 수 있지만, calibre API를 사용해 Python 코드를 작성하는 법을 알아야 한다.

PTM 템플릿은 다음으로 시작한다:

python:
def evaluate(book, context):
# book is a calibre metadata object
# context is an instance of calibre.utils.formatter.PythonTemplateContext,
# which currently contains the following attributes:
# db: a calibre legacy database object.
# globals: the template global variable dictionary.
# arguments: is a list of arguments if the template is called by a GPM template, otherwise None.
# funcs: used to call Built-in/User functions and Stored GPM/Python templates.
# Example: context.funcs.list_re_group()

# your Python code goes here
return 'a string'

보통 오른쪽 클릭으로 여는 문맥 메뉴를 사용해 위 텍스트를 템플릿에 추가할 수 있다. 주석은 의미가 없으므로 제거해도 된다. Python 들여쓰기를 사용해야 한다.

context 객체는 문맥 내용 문자열을 반환하는 ``str(context)``와, 문맥의 속성 이름 목록을 반환하는 ``context.attributes``를 지원한다.

context.funcs 속성을 사용하면 Built-in 및 User 템플릿 함수, 저장된 GPM/Python 템플릿을 호출할 수 있어 코드 안에서 직접 실행할 수 있다. 함수는 이름으로 가져온다. 이름이 Python 키워드와 충돌하면 끝에 밑줄을 하나 붙여라. 예:

context.funcs.list_re_group()
context.funcs.assert_()

다음은 어떤 시리즈의 모든 저자 목록을 만드는 PTM 템플릿 예이다. 이 목록은 다른 열로부터 만들어진 열, 태그처럼 동작`하는 열에 저장된다. 이 값은 :guilabel:`책 정보`에 표시되며 :guilabel:`각 줄에 별도 표시 옵션이 켜져 있다(환경설정 → 모양새 → 책 정보). 이 옵션은 목록이 쉼표로 구분되어 있어야 한다. 이 요구를 만족시키기 위해 템플릿은 저자 이름 안의 쉼표를 세미콜론으로 바꾼 다음, 저자들의 쉼표 구분 목록을 만든다. 이 과정은 함수 ``get_item_names()``의 결과를 authors 필드에 있는 값으로 제한한다:

python:
def evaluate(book, context):
if book.series is None:
return ''
db = context.db.new_api
ans = set()
# Get the list of books in the series
ids = db.search(f'series:"={book.series}"', '')
if ids:
# Get all the author_sort values for the books in the series
author_sorts = (v for v in db.all_field_for('author_sort', ids).values())
# Add the names to the result set, removing duplicates
for aus in author_sorts:
ans.update(v.strip() for v in aus.split('&'))
# Make a sorted comma-separated string from the result set
return ', '.join(v.replace(',', ';') for v in sorted(ans))

:guilabel:`책 정보`의 출력은 다음과 같다:

템플릿과 URL

템플릿을 사용해 URL을 구성할 수 있다. 여기서는 두 가지 경우를 설명한다:

- 사용자 정의 열 책 정보 검색 URL

- calibre URL 스킴

사용자 정의 열 책 정보 검색 URL

사용자 정의 열을 만들 때 템플릿을 사용해 책 정보`에서 사용할 URL을 지정할 수 있다. 예를 들어 `번역가`용 사용자 정의 열이 있다면 번역가 사이트로 이동하는 URL을 정의할 수 있다. 책 정보 검색 URL은 `Text, Enumerated, Series, Column built from other column 형식 열에 제공할 수 있다.

:guilabel:`책 정보`에서 `검색 템플릿`이 있는 항목을 클릭하면 템플릿이 평가된다. 이때 일반적인 책 메타데이터와 함께 다음 세 개의 추가 필드도 제공된다:

- item_value: 클릭한 항목의 값.

- item_value_quoted: 클릭한 항목의 URL 인코딩된 값. 특수 문자는 URL에서 유효하도록 이스케이프되며 공백은 ``’+’``(플러스) 기호로 바뀐다.

- item_value_no_plus: 클릭한 항목의 URL 인코딩된 값. 특수 문자는 URL에서 유효하도록 이스케이프되며 공백은 플러스가 아닌 ``%20``으로 바뀐다.

URL을 구성하는 방법은 여러 가지가 있다. 다음 예에서는 Wikipedia를 사용한다.

가장 단순한 방법은 기본 템플릿이다:

https://en.wikipedia.org/w/index.php?search={item_value_encoded}

경우에 따라 더 많은 처리가 필요할 수 있다. 처리 복잡도에 따라 사용할 수 있는 템플릿 함수는 네 가지이다.

- make_url(path, [query_name, query_value]+) – 질의 URL을 구성하는 가장 쉬운 방법입니다. 질의할 웹 사이트와 페이지를 나타내는 path 와, 질의를 구성하는 query_name, query_value 쌍을 사용합니다. 일반적으로 query_value 는 URL 인코딩되어야 합니다. 이 함수는 항상 값을 인코딩하며 공백은 항상 '+' 기호로 바꿉니다.

- make_url_extended(...) – 이 함수는 make_url() 과 비슷하지만 URL 구성 요소를 더 세밀하게 제어할 수 있습니다. URL 의 구성 요소는 다음과 같습니다:

scheme:://authority/path?query string.

자세한 내용은 Wikipedia 의 Uniform Resource Locator [https://en.wikipedia.org/wiki/URL] 문서를 참조하십시오.

이 함수에는 두 가지 형태가 있습니다:

make_url_extended(scheme, authority, path, [query_name, query_value]+)

그리고

make_url_extended(scheme, authority, path, query_string)

- query_string([query_name, query_value, how_to_encode]+) – query_name, query_value, how_to_encode 삼중항으로부터 URL 질의 문자열을 구성해 반환합니다. 질의 문자열은 query_name=query_value 형태의 항목들이 이어진 것이며, query_value 는 지정된 방식대로 URL 인코딩됩니다. 각 항목은 ``’&’``(앰퍼샌드) 문자로 구분됩니다.

- encode_for_url(value, use_plus) – use_plus 설정에 따라 URL 에 사용할 수 있게 value 를 인코딩해 반환합니다. 값은 먼저 URL 인코딩되며, 이어서 use_plus 가 0 이면 공백을 '+' 로, 아니면 %20 으로 바꿉니다.

예를 들어 성, 이름 형식으로 이름을 저장하는 사용자 정의 열 Translators`(``#translators`)가 있다고 하자. URL을 만들 때 이 이름을 이름 성 형식으로 바꿔야 할 수 있다. 이때 make_url 함수를 사용할 수 있다:

program: make_url('https://en.wikipedia.org/w/index.php', 'search', swap_around_comma($item_value))

번역가 이름이 `Boy-Żeleński, Tadeusz`라고 가정하면 위 템플릿은 다음 링크를 만든다:

https://en.wikipedia.org/w/index.php?search=Tadeusz+Boy-%C5%BBele%C5%84ski

이제 사람 이름은 이름이 먼저 오고, 공백은 플러스로 바뀌며, 성에 있는 비영어 문자는 URL 인코딩된다는 점에 유의하라.

추가 처리의 복잡도에 따라 make_url_extended, query_string, encode_for_url 함수도 유용할 수 있다.

calibre URL 스킴

Calibre는 calibre 라이브러리를 탐색하기 위한 여러 종류의 URL을 지원한다. 이 절에서는 템플릿을 사용해 일부 URL을 구성하는 방법을 보여 준다. 사용 가능한 URL의 자세한 내용은 :doc:`url_scheme`를 참조하라.

특정 라이브러리로 전환한다. 이 URL의 문법은 다음과 같다:

calibre://switch-library/Library_Name

``Library_Name``은 열고 싶은 calibre 라이브러리의 이름으로 바꿔야 한다. 라이브러리 이름은 창의 제목 표시줄에 표시된다. 이것은 라이브러리의 파일 경로가 아니라 단순한 이름이다. 제목 표시줄에 보이는 대로, 대소문자까지 포함해 정확히 입력해야 한다. 문자 ```(밑줄)는 현재 라이브러리를 뜻한다. 이름에 공백이나 특수 문자가 포함되어 있다면 다음 예와 같이 :ref:`ff_to_hex 함수를 사용해 hex 인코딩해야 한다:

program: strcat('calibre://switch-library/_hex_-', to_hex(current_library_name()))

이 템플릿은 다음 URL을 생성한다:

calibre://switch-library/_hex_-4c6962726172792e746573745f736d616c6c

current_library_name() 함수를 실제 라이브러리 이름으로 바꿔 쓸 수도 있다:

program: strcat('calibre://switch-library/_hex_-', to_hex('Library.test_small'))

책을 표시하는 링크. 이 링크는 calibre 라이브러리에서 특정 책을 선택한다. 이 URL의 문법은 다음과 같다:

calibre://show-book/Library_Name/book_id

``book id``는 책의 숫자형 calibre ID이며 템플릿에서는 ``$id``로 사용할 수 있다. 위와 마찬가지로 라이브러리 이름은 hex 인코딩이 필요할 수 있다. 예:

program: strcat('calibre://show-book/_hex_-', to_hex(current_library_name()), '/', $id)

이 템플릿은 다음 URL을 만든다:

calibre://show-book/_hex_-4c6962726172792e746573745f736d616c6c/1353

책 검색하기. 이 링크는 지정된 calibre 라이브러리에서 책을 검색한다. 이 URL의 문법은 다음과 같다:

calibre://search/Library_Name?q=query
calibre://search/Library_Name?eq=hex_encoded_query

`query`는 유효한 calibre 검색 식이면 무엇이든 될 수 있다. 공백이나 특수 문자가 들어간 질의는 반드시 hex 인코딩해야 하며, 실제로는 거의 모두 해당된다. 예를 들어 ‘AA’로 시작하는 계층형 태그를 찾는 calibre 검색 식은 ``tags:”=.AA”``이다. 다음 템플릿은 이 식의 검색 URL을 만든다:

program: strcat('calibre://search/_hex_-', to_hex(current_library_name()), '?eq=', to_hex('tags:"=.AA"'))

결과 URL은 다음과 같다:

calibre://search/_hex_-4c6962726172792e746573745f736d616c6c?eq=746167733a223d2e414122

다음은 strcat 대신 :ref:ff_make_url_extended 함수를 사용해 같은 URL을 만든 예이다:

program: make_url_extended('calibre', '', 'search/_hex_-' & to_hex(current_library_name()),
'eq', to_hex('tags:"=.AA"'))

어떤 라이브러리의 특정 책에 대한 책 정보 창을 연다. 이 URL의 문법은 다음과 같다:

calibre://book-details/Library_Name/book_id

예시 템플릿은 다음과 같다:

program: strcat('calibre://book-details/_hex_-', to_hex(current_library_name()), '/', $id)

이 템플릿은 다음 URL을 만든다:

calibre://book-details/_hex_-4c6962726172792e746573745f736d616c6c/1353

저자/시리즈 등의 값에 연결된 노트를 연다. 이 URL의 문법은 다음과 같다:

calibre://book-details/Library_Name/Field_Name/id_Item_Id
calibre://book-details/Library_Name/Field_Name/hex_Hex_Encoded_Item_Name

Field_Name``은 필드의 lookup name이다. 필드가 사용자 정의 열이면 ``# 문자를 밑줄(_)로 바꿔야 한다. Item_Id``는 그 필드 값의 내부 숫자 ID이다. ``Item_Id``를 반환하는 템플릿 함수는 없으므로, 템플릿에서는 보통 두 번째 형식인 ``Hex_Encoded_Item_Name``을 사용한다. 다음은 ``#authtest 필드에서 ``Boy-Żeleński, Tadeusz``의 노트를 여는 예시 템플릿이다:

program: strcat('calibre://show-note/_hex_-', to_hex(current_library_name()),
'/_authtest/hex_', to_hex('Boy-Żeleński, Tadeusz'))

이 템플릿은 다음 URL을 만든다:

calibre://show-note/_hex_-4c6962726172792e746573745f736d616c6c/_authtest/hex_426f792dc5bb656c65c584736b692c205461646575737a

저장된 템플릿

:ref:`General Program Mode <general_mode>`와 :ref:`Python Template Mode <python_mode>`는 모두 템플릿을 저장하고 다른 템플릿에서 마치 저장된 함수처럼 그 템플릿을 호출하는 기능을 지원한다. 템플릿은 :guilabel:`환경설정->고급->템플릿 함수`에서 저장한다. 자세한 정보는 해당 대화상자에 나와 있다. 템플릿 호출은 함수 호출과 동일하게 하며, 필요하면 위치 인수를 전달할 수 있다. 인수는 어떤 식이든 될 수 있다. 저장된 템플릿 이름이 ``foo``라고 가정할 때의 호출 예는 다음과 같다:

- foo() – 인수 없이 템플릿을 호출한다.

- foo(a, b) – 두 변수 ``a``와 ``b``의 값을 인수로 전달하며 템플릿을 호출한다.

- foo(if field('series') then field('series_index') else 0 fi) – 책에 ``series``가 있으면 ``series_index``를 전달하고, 그렇지 않으면 값 ``0``을 전달한다.

GPM에서는 arguments 함수를 사용해 저장된 템플릿 호출 시 전달된 인수를 가져온다. 이 함수는 지역 변수를 선언하고 초기화하므로 사실상 매개변수 역할을 한다. 변수는 위치 기반이며, 호출에서 같은 위치에 전달된 매개변수 값을 받는다. 해당 매개변수가 전달되지 않았으면 ``arguments``는 그 변수에 제공된 기본값을 할당한다. 기본값도 없다면 빈 문자열이 설정된다. 예를 들면:

arguments(key, alternate='series')

예, 다시 저장된 템플릿 이름이 ``foo``라고 가정하면:

- foo('#myseries') – 인수 ``key``에는 값 ``’myseries’``가 할당되고, 인수 ``alternate``에는 기본값 ``’series’``가 할당된다.

- foo('series', '#genre') – 변수 ``key``에는 값 ``’series’``가, 변수 ``alternate``에는 값 ``’#genre’``가 할당된다.

- foo() – 변수 ``key``에는 빈 문자열이, 변수 ``alternate``에는 값 ``’series’``가 할당된다.

PTM에서는 인수가 arguments 매개변수로 전달되며, 이는 문자열 목록이다. 기본값을 지정하는 방법은 없다. arguments 목록 길이를 확인해 인수 개수가 기대한 것과 맞는지 직접 검사해야 한다.

저장된 템플릿을 쉽게 시험하는 방법은 Template tester 대화상자를 사용하는 것이다. 쉽게 접근하려면 환경설정 → 고급 → 키보드 단축키 → Template tester`에서 단축키를 지정하라. ``Stored templates` 대화상자에도 단축키를 지정하면 테스터와 저장된 템플릿 소스 편집 사이를 더 빠르게 오갈 수 있다.

템플릿에 추가 정보 제공하기

개발자는 응용프로그램별 책 메타데이터나 프로세서가 수행해야 하는 작업 같은 추가 정보를 템플릿 처리기에 전달하도록 선택할 수 있다. 템플릿은 이 정보에 접근하여 평가 중에 사용할 수 있다.

개발자: 추가 정보를 전달하는 방법

추가 정보는 variable_name: variable_value 쌍을 담은 Python 사전이며, 값은 문자열이어야 한다. 템플릿은 이 사전에 접근하여 ``variable_name``이라는 이름의 지역 변수를 만들고 그 값으로 ``variable_value``를 넣을 수 있다. 사용자는 그 이름을 바꿀 수 없으므로, 다른 템플릿 지역 변수와 충돌하지 않도록 앞에 밑줄을 붙이는 등 안전한 이름을 쓰는 것이 좋다.

이 사전은 이름 있는 매개변수 global_vars=your_dict``를 사용해 템플릿 처리기(``formatter)에 전달된다. 전체 메서드 시그니처는 다음과 같다:

def safe_format(self, fmt, kwargs, error_value, book,
column_name=None, template_cache=None,
strip_results=True, template_functions=None,
global_vars={})

템플릿 작성자: 추가 정보에 접근하는 방법

템플릿에서는 다음 템플릿 함수를 사용해 추가 정보(globals 사전)에 접근한다:

globals(id[=expression] [, id[=expression]]*)

여기서 ``id``는 합법적인 변수 이름이면 무엇이든 된다. 이 함수는 개발자가 제공한 추가 정보에 해당 이름이 있는지 확인한다. 있다면 제공된 값을 같은 이름의 템플릿 지역 변수에 할당한다. 그 이름이 추가 정보에 없고 ``expression``이 제공되면 그 식을 평가한 결과를 지역 변수에 할당한다. 값도 식도 없으면 이 함수는 지역 변수에 빈 문자열을 할당한다.

템플릿은 다음 템플릿 함수를 사용해 globals 사전에 값을 설정할 수 있다:

set_globals(id[=expression] [, id[=expression]]*)

이 함수는 globals 사전에 id:value 키:값 쌍을 설정한다. 여기서 ``value``는 템플릿 지역 변수 ``id``의 값이다. 해당 지역 변수가 존재하지 않으면 ``expression``을 평가한 결과가 ``value``로 사용된다.

모드 간 차이점에 대한 참고

세 가지 프로그램 모드, 즉 Single Function Mode <single_mode>`(SFM), :ref:`Template Program Mode <template_mode>`(`TPM), General Program Mode <general_mode>`(`GPM)는 서로 다르게 동작한다. SFM은 ‘단순함’을 목표로 하기 때문에 프로그래밍 언어의 많은 요소를 숨긴다.

차이점:

- SFM에서는 열의 값이 템플릿에 포함된 함수의 첫 번째 인수로 항상 ‘보이지 않게’ 전달된다.

- SFM은 변수와 문자열의 차이를 지원하지 않으며, 모든 값은 문자열이다.

- 다음 SFM 템플릿은 시리즈 이름 또는 문자열 “no series”를 반환한다:

{series:ifempty(no series)}

`TPM`에서 이에 해당하는 템플릿은 다음과 같다

{series:'ifempty($, 'no series')'}

`GPM`에서 이에 해당하는 템플릿은 다음과 같다:

program: ifempty(field('series'), 'no series')

ifempty``의 첫 번째 인수는 ``series 필드의 값이다. 두 번째 인수는 문자열 ``no series``이다. SFM에서는 첫 번째 인수, 즉 필드 값이 자동으로 전달된다(보이지 않는 인수).

- booksize() 및 ``current_library_name()``처럼 인수를 받지 않는 템플릿 함수도 여럿 있다. 하지만 ‘보이지 않는 인수’ 때문에 이런 함수는 SFM에서 사용할 수 없다.

- 한 함수가 다른 함수를 호출해 인수를 계산하는 중첩 함수는 SFM에서 사용할 수 없다. 예를 들어 시리즈 값을 대문자로 바꾼 뒤 처음 5글자만 반환하려는 다음 템플릿은 SFM에서 동작하지 않는다:

{series:uppercase(substr(0,5))}

- `TPM`과 `GPM`은 중첩 함수를 지원한다. 위의 템플릿을 `TPM`으로 쓰면 다음과 같다:

{series:'uppercase(substr($, 0,5))'}

`GPM`에서는 다음과 같다:

program: uppercase(substr(field('series'), 0,5))

- 위 Template Program Mode 절에서 언급했듯이, TPM 문자열 리터럴 안에서 { 와 } 문자를 사용하면 템플릿 처리기가 이를 문자 대신 템플릿 경계로 취급하려 하기 때문에 오류나 예상치 못한 결과가 날 수 있다. 경우에 따라 {``를 ``[[``로, ``}``를 `]]`로 바꿀 수 있지만 항상 되는 것은 아니다. 일반적으로 프로그램에 ``{ 와 } 문자가 포함되어 있다면 `General Program Mode`를 사용해야 한다.

사용자 정의 Python 템플릿 함수

사용자는 템플릿 처리기에 자신만의 Python 함수를 추가할 수 있다. 이런 함수는 세 가지 템플릿 프로그래밍 모드 어디에서나 사용할 수 있다. 함수는 :guilabel:`환경설정 -> 고급 -> 템플릿 함수`로 가서 추가한다. 해당 대화상자에 지침이 표시된다. 비슷한 목적으로 `Python Templates`를 사용할 수도 있다. 사용자 정의 함수 호출은 Python 템플릿 호출보다 빠르므로, 작업 복잡도에 따라 더 효율적일 수 있다.

서로 다른 문맥에서 템플릿을 사용할 때의 특별 참고사항

GUI에서(다른 열로부터 만들어진 열 및 템플릿 검색):

- GPM 템플릿은 이전과 같이 동작한다.

- Python 템플릿은 calibre 데이터베이스에 완전히 접근할 수 있다.

아이콘 규칙에서:

- 아이콘 규칙 템플릿에는 책 데이터가 없으므로 format_date_field, list_count_field, check_yes_no 같은 필드 기반 함수는 동작하지 않는다.

콘텐츠 서버에서:

- 템플릿은 새 API에는 접근할 수 있지만 이전 API(LibraryDatabase)에는 접근할 수 없다.

- 위 이유로 인해 다음 formatter 함수들은 GPM 템플릿(합성 열, 아이콘 규칙 등)에서 정상 동작이 보장되지 않으므로 콘텐츠 서버를 사용할 경우 피해야 한다:

- connected_device_name

- connected_device_uuid

- current_virtual_library_name

- is_marked

- virtual_libraries

저장/전송 템플릿에 대한 특별 참고사항

템플릿을 디스크에 저장 또는 기기로 보내기 템플릿으로 사용할 때는 특별한 처리가 적용된다. 필드 값은 슬래시를 포함해 파일 시스템에서 특별한 의미를 가진 문자를 밑줄로 바꾸는 방식으로 정리된다. 즉, 필드 텍스트로 폴더를 만들 수 없다. 그러나 접두사/접미사 문자열의 슬래시는 바뀌지 않으므로, 이 문자열에 슬래시가 있으면 폴더가 생성된다. 이 점을 이용하면 시리즈 정보나 사용자 정의 열 값을 기준으로 가변 깊이의 폴더 구조를 만들 수 있다.

예를 들어 `series/series_index - title`이라는 폴더 구조를 원한다고 하자. 단, 시리즈가 없으면 제목이 최상위 폴더에 있어야 한다. 이를 위한 템플릿은 다음과 같다:

{series:||/}{series_index:|| - }{title}

슬래시와 하이픈은 series가 비어 있지 않을 때만 나타난다.

lookup 함수를 사용하면 더 복잡한 처리도 가능하다. 예를 들어 책에 시리즈가 있으면 series/series index - title.fmt 구조를 원하고, 시리즈가 없으면 genre/author_sort/title.fmt 구조를 원한다고 하자. 장르도 없으면 ‘Unknown’을 사용하고 싶다. 즉, series 값에 따라 완전히 다른 두 경로를 사용하고 싶은 것이다.

이를 위해 우리는:

1. ``{series}/{series_index} - {title}``를 담는 합성 필드(lookup name은 #aa)를 만든다. 시리즈가 비어 있지 않으면 이 템플릿은 `series/series_index - title`을 만든다.

2. ``{#genre:ifempty(Unknown)}/{author_sort}/{title}``를 담는 합성 필드(lookup name은 #bb)를 만든다. 이 템플릿은 `genre/author_sort/title`을 만들며, 비어 있는 장르는 `Unknown`으로 대체된다.

3. 저장 템플릿을 ``{series:lookup(.,#aa,#bb)}``로 설정한다. 이 템플릿은 series가 비어 있지 않으면 합성 필드 ``#aa``를, 비어 있으면 합성 필드 ``#bb``를 선택한다. 따라서 `series`가 비어 있는지 여부에 따라 완전히 다른 두 저장 경로를 가지게 된다.

팁

- Template Tester를 사용해 템플릿을 시험하라. 라이브러리의 책 문맥 메뉴에 테스터를 추가하거나 키보드 단축키를 지정해 두면 편리하다.

- 템플릿은 원하는 템플릿으로 만들어진 합성 열을 참조함으로써 다른 템플릿을 사용할 수 있다. 또는 Stored Templates를 사용할 수도 있다.

- 플러그보드에서는 특수 템플릿 ``{}``를 사용해 필드를 빈 값(또는 빈 값에 해당하는 값)으로 설정할 수 있다. 이 템플릿은 항상 빈 문자열로 평가된다.

- 위에서 설명한, 값이 0이어도 숫자를 표시하는 기법은 표준 필드 series_index에도 적용된다.

템플릿 기능 참조

Reference for all built-in template language functions

GUI 함수

- selected_books
- selected_column
- show_dialog
- sort_book_ids
- width_from_pages

URL 함수

- encode_for_url
- make_url
- make_url_extended
- query_string
- to_hex
- urls_from_identifiers

값 반복 처리

- first_non_empty
- lookup
- switch
- switch_if

값 서식 지정

- f_string
- finish_formatting
- format_date
- format_date_field
- format_duration
- format_number
- human_readable
- rating_to_stars

관계 연산

- cmp
- first_matching_cmp
- strcmp
- strcmpcase

기타

- arguments
- assign
- globals
- is_dark_mode
- print
- set_globals

날짜 함수

- date_arithmetic
- days_between
- today

대소문자 변경

- capitalize
- lowercase
- titlecase
- uppercase

데이터베이스 함수

- annotation_count
- approximate_formats
- book_count
- book_values
- extra_file_modtime
- extra_file_names
- extra_file_size
- formats_modtimes
- formats_path_segments
- formats_paths
- formats_sizes
- get_link
- get_note
- has_extra_files
- has_note
- reading_progress

메타데이터에서 값 가져오기

- author_links
- author_sorts
- booksize
- connected_device_name
- connected_device_uuid
- current_library_name
- current_library_path
- current_virtual_library_name
- field
- has_cover
- is_marked
- language_codes
- language_strings
- ondevice
- raw_field
- raw_list
- series_sort
- user_categories
- virtual_libraries

목록 조작

- list_count
- list_count_field
- list_count_matching
- list_difference
- list_equals
- list_intersection
- list_join
- list_re
- list_re_group
- list_remove_duplicates
- list_sort
- list_split
- list_union
- range
- subitems
- sublist

목록 조회

- identifier_in_list
- list_contains
- list_item
- select
- str_in_list

문자열 조작

- character
- check_yes_no
- contains
- field_exists
- ifempty
- re
- re_group
- shorten
- strcat
- strcat_max
- strlen
- substr
- swap_around_articles
- swap_around_comma
- test
- transliterate

불리언

- and
- not
- or

산술

- add
- ceiling
- divide
- floor
- fractional_part
- mod
- multiply
- round
- subtract

재귀

- eval

- template

API of the Metadata objects

- Metadata

- STANDARD_METADATA_FIELDS

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 튜토리얼 »
- calibre 템플릿 언어 »

- Reference for all built-in template language functions

Reference for all built-in template language functions

Here, we document all the built-in functions available in the calibre template language. Every function is implemented as a class in python and you can click the source links to see the source code, in case the documentation is insufficient. The functions are arranged in logical groups by type.

GUI 함수

- selected_books

- selected_column

- show_dialog

- sort_book_ids

- width_from_pages

URL 함수

- encode_for_url

- make_url

- make_url_extended

- query_string

- to_hex

- urls_from_identifiers

값 반복 처리

- first_non_empty

- lookup

- switch

- switch_if

값 서식 지정

- f_string

- finish_formatting

- format_date

- format_date_field

- format_duration

- format_number

- human_readable

- rating_to_stars

관계 연산

- cmp

- first_matching_cmp

- strcmp

- strcmpcase

기타

- arguments

- assign

- globals

- is_dark_mode

- print

- set_globals

날짜 함수

- date_arithmetic

- days_between

- today

대소문자 변경

- capitalize

- lowercase

- titlecase

- uppercase

데이터베이스 함수

- annotation_count

- approximate_formats

- book_count

- book_values

- extra_file_modtime

- extra_file_names

- extra_file_size

- formats_modtimes

- formats_path_segments

- formats_paths

- formats_sizes

- get_link

- get_note

- has_extra_files

- has_note

- reading_progress

메타데이터에서 값 가져오기

- author_links

- author_sorts

- booksize

- connected_device_name

- connected_device_uuid

- current_library_name

- current_library_path

- current_virtual_library_name

- field

- has_cover

- is_marked

- language_codes

- language_strings

- ondevice

- raw_field

- raw_list

- series_sort

- user_categories

- virtual_libraries

목록 조작

- list_count

- list_count_field

- list_count_matching

- list_difference

- list_equals

- list_intersection

- list_join

- list_re

- list_re_group

- list_remove_duplicates

- list_sort

- list_split

- list_union

- range

- subitems

- sublist

목록 조회

- identifier_in_list

- list_contains

- list_item

- select

- str_in_list

문자열 조작

- character

- check_yes_no

- contains

- field_exists

- ifempty

- re

- re_group

- shorten

- strcat

- strcat_max

- strlen

- substr

- swap_around_articles

- swap_around_comma

- test

- transliterate

불리언

- and

- not

- or

산술

- add

- ceiling

- divide

- floor

- fractional_part

- mod

- multiply

- round

- subtract

재귀

- eval

- template

API of the Metadata objects

GUI 함수

selected_books

class calibre.utils.formatter_functions.BuiltinSelectedBooks[소스]

selected_books([sorted_by, ascending]) – 현재 선택된 책들의 책 ID 목록을 선택 순서대로 반환합니다. / / 이 함수는 GUI 에서만 사용할 수 있습니다.

selected_column

class calibre.utils.formatter_functions.BuiltinSelectedColumn[소스]

selected_column() – 현재 선택된 셀이 들어 있는 열의 조회 이름을 반환합니다. 선택된 셀이 없으면 '' 를 반환합니다. / / 이 함수는 GUI 에서만 사용할 수 있습니다.

show_dialog

class calibre.utils.formatter_functions.BuiltinShowDialog[소스]

show_dialog(html_or_text) – HTML 또는 텍스트를 포함한 대화상자를 표시합니다. 사용자가 확인을 누르면 '1' 을, 취소를 누르면 '' 를 반환합니다. / / 이 함수는 GUI 에서만 사용할 수 있습니다.

sort_book_ids

class calibre.utils.formatter_functions.BuiltinSortBookIds[소스]

sort_book_ids(book_ids, sorted_by, ascending [, sorted_by, ascending]*) – book_ids 목록을 sorted_by 로 지정한 조회 이름의 열 기준으로 ascending 이 지정하는 순서대로 정렬해 반환합니다. 여러 정렬 기준을 순서대로 지정할 수 있습니다.

width_from_pages

class calibre.utils.formatter_functions.BuiltinWidthFromPages[소스]

width_from_pages(value [, num_of_pages_for_max_width, logarithmic_factor, default_width]) – 페이지 수가 주어졌을 때 책등 너비를 '0' 과 '1' 사이의 비율로 반환합니다. 책 목록의 표지 격자 등에서 책등 두께를 추정하는 데 사용됩니다.

URL 함수

encode_for_url

class calibre.utils.formatter_functions.BuiltinEncodeForURL[소스]

encode_for_url(value, use_plus) – use_plus 설정에 따라 URL 에 사용할 수 있게 value 를 인코딩해 반환합니다. 값은 먼저 URL 인코딩되며, 이어서 use_plus 가 0 이면 공백을 '+' 로, 아니면 %20 으로 바꿉니다.

make_url

class calibre.utils.formatter_functions.BuiltinMakeUrl[소스]

make_url(path, [query_name, query_value]+) – 질의 URL을 구성하는 가장 쉬운 방법입니다. 질의할 웹 사이트와 페이지를 나타내는 path 와, 질의를 구성하는 query_name, query_value 쌍을 사용합니다. 일반적으로 query_value 는 URL 인코딩되어야 합니다. 이 함수는 항상 값을 인코딩하며 공백은 항상 '+' 기호로 바꿉니다.

query_name, query_value 쌍을 하나 이상 제공해야 합니다.

예: 저자 Niccolò Machiavelli 에 대한 Wikipedia 검색 URL을 구성하려면:

make_url('https://en.wikipedia.org/w/index.php', 'search', 'Niccolò Machiavelli')

다음이 반환됩니다:

https://en.wikipedia.org/w/index.php?search=Niccol%C3%B2+Machiavelli

사용자 정의 열의 책 상세 URL 템플릿을 작성하는 경우, 클릭한 필드의 값을 얻으려면 $item_name 또는 field('item_name') 을 사용하십시오. 예를 들어 Niccolò Machiavelli 을 클릭한 경우 다음과 같이 URL 을 만들 수 있습니다:

make_url('https://en.wikipedia.org/w/index.php', 'search', $item_name)

make_url_extended(), query_string(), encode_for_url() 함수도 참고하십시오.

make_url_extended

class calibre.utils.formatter_functions.BuiltinMakeUrlExtended[소스]

make_url_extended(...) – 이 함수는 make_url() 과 비슷하지만 URL 구성 요소를 더 세밀하게 제어할 수 있습니다. URL 의 구성 요소는 다음과 같습니다:

scheme:://authority/path?query string.

자세한 내용은 Wikipedia 의 Uniform Resource Locator [https://en.wikipedia.org/wiki/URL] 문서를 참조하십시오.

이 함수에는 두 가지 형태가 있습니다:

make_url_extended(scheme, authority, path, [query_name, query_value]+)

그리고

make_url_extended(scheme, authority, path, query_string)

이 함수는 scheme, authority, path 와 query_string 또는 질의 인수 쌍으로 만든 질의 문자열을 사용해 URL 을 구성하여 반환합니다. authority 는 비워 둘 수 있으며, calibre scheme URL 이 그 예입니다. query_string 또는 최소 하나의 query_name, query_value 쌍을 반드시 제공해야 합니다. query_string 을 제공했는데 그것이 빈 문자열이면 결과 URL 에는 질의 문자열 구간이 포함되지 않습니다.

예 1: 저자 Niccolò Machiavelli 에 대한 Wikipedia 검색 URL 구성:

make_url_extended('https', 'en.wikipedia.org', '/w/index.php', 'search', 'Niccolò Machiavelli')

다음이 반환됩니다:

https://en.wikipedia.org/w/index.php?search=Niccol%C3%B2+Machiavelli

query_string 과 함께 make_url_extended() 를 사용하는 예는 query_string() 함수를 참조하십시오.

사용자 정의 열의 책 상세 URL 템플릿을 작성하는 경우, 클릭한 필드의 값을 얻으려면 $item_name 또는 field('item_name') 을 사용하십시오. 예를 들어 Niccolò Machiavelli 을 클릭한 경우 다음과 같이 URL 을 만들 수 있습니다:

make_url_extended('https', 'en.wikipedia.org', '/w/index.php', 'search', $item_name')

make_url(), query_string(), encode_for_url() 함수도 참고하십시오.

query_string

class calibre.utils.formatter_functions.BuiltinQueryString[소스]

query_string([query_name, query_value, how_to_encode]+) – query_name, query_value, how_to_encode 삼중항으로부터 URL 질의 문자열을 구성해 반환합니다. 질의 문자열은 query_name=query_value 형태의 항목들이 이어진 것이며, query_value 는 지정된 방식대로 URL 인코딩됩니다. 각 항목은 ``’&’``(앰퍼샌드) 문자로 구분됩니다.

how_to_encode 가 0 이면 query_value 를 인코딩하고 공백을 '+'``(플러스)로 바꿉니다. ``1 이면 query_value 를 인코딩하되 공백을 %20 으로 바꿉니다. 2 이면 query_value 를 변경하지 않고 그대로 반환합니다. 인코딩도 하지 않고 공백도 바꾸지 않습니다. query_value 를 인코딩하지 않되 공백만 바꾸고 싶다면 re($series, ' ', '%20') 와 같이 re() 함수를 사용하십시오.

이 함수는 질의 문자열 각 부분이 어떻게 구성되는지 세밀하게 제어해야 할 때 사용합니다. 그런 다음 결과 질의 문자열을 make_url_extended() 에 사용할 수 있습니다. 예:

make_url_extended(
'https', 'your_host', 'your_path',
query_string('encoded', 'Hendrik Bäßler', 0, 'unencoded', 'Hendrik Bäßler', 2))

이 경우 다음이 생성됩니다:

https://your_host/your_path?encoded=Hendrik+B%C3%A4%C3%9Fler&unencoded=Hendrik Bäßler

query_name, query_value, how_to_encode 삼중항을 하나 이상 제공해야 하며, 원하는 만큼 더 많이 지정할 수 있습니다.

반환값은 지정한 모든 항목이 포함된 URL 질의 문자열입니다. 예: name1=val1[&nameN=valN]*. 반환값에는 '?' path / query string 구분자가 포함되지 않습니다.

사용자 정의 열의 책 상세 URL 템플릿을 작성하는 경우, 클릭한 필드의 인코딩되지 않은 값을 얻으려면 $item_name 또는 field('item_name') 을 사용하십시오. 공백을 플러스로 바꿔 이미 인코딩된 값은 item_value_quoted 로, 공백을 %20 으로 바꿔 이미 인코딩된 값은 item_value_no_plus 로 사용할 수 있습니다.

make_url(), make_url_extended(), encode_for_url() 함수도 참고하십시오.

to_hex

class calibre.utils.formatter_functions.BuiltinToHex[소스]

to_hex(val) – 문자열 val 을 16진수로 인코딩해 반환합니다. calibre URL 을 구성할 때 유용합니다.

urls_from_identifiers

class calibre.utils.formatter_functions.BuiltinUrlsFromIdentifiers[소스]

urls_from_identifiers(identifiers, sort_results) – identifiers 라는 콤마 구분 식별자 목록을 받아 URL 목록을 콤마로 구분해 반환합니다. 각 식별자는 id_name:id_value 형식입니다. sort_results 가 참이면 결과를 정렬합니다.

값 반복 처리

first_non_empty

class calibre.utils.formatter_functions.BuiltinFirstNonEmpty[소스]

first_non_empty(value [, value]*) – 비어 있지 않은 첫 번째 value 를 반환합니다. 모든 값이 비어 있으면 빈 문자열을 반환합니다. 인수 개수는 제한이 없습니다.

lookup

class calibre.utils.formatter_functions.BuiltinLookup[소스]

lookup(value, [ pattern, key, ]* else_key) – 패턴들을 순서대로 value 와 대조합니다. 어떤 pattern 이 일치하면 key 가 가리키는 필드 값을 반환합니다. 일치하는 패턴이 없으면 else_key 가 가리키는 필드 값을 반환합니다.

switch

class calibre.utils.formatter_functions.BuiltinSwitch[소스]

switch(value, [patternN, valueN,]+ else_value) – 각 patternN, valueN 쌍에 대해 value 가 정규식 patternN 과 일치하는지 검사하고 일치하면 연결된 valueN 을 반환합니다. 어느 것도 일치하지 않으면 else_value 를 반환합니다.

switch_if

class calibre.utils.formatter_functions.BuiltinSwitchIf[소스]

switch_if([test_expression, value_expression,]+ else_expression) – 각 test_expression, value_expression 쌍에 대해 test_expression 이 참(비어 있지 않음)인지 검사하고 참이면 value_expression 결과를 반환합니다. 어느 것도 참이 아니면 else_expression 결과를 반환합니다.

값 서식 지정

f_string

class calibre.utils.formatter_functions.BuiltinFString[소스]

f_string(string) – Python 이 f 문자열을 해석하는 방식과 비슷하게 string 을 해석합니다. 긴 str & str 또는 strcat(a,b,c) 표현을 단순화하려는 용도입니다.

중괄호({ 와 }) 사이의 텍스트는 General Program Mode 템플릿 식이어야 합니다. 식은 식 목록일 수도 있으며, 현재 컨텍스트(현재 책과 지역 변수)에서 평가됩니다. 중괄호 밖의 텍스트는 변경 없이 그대로 전달됩니다.

예:

- f_string('Here is the title: {$title}') - 현재 책의 제목으로 {$title} 을 치환한 문자열을 반환합니다. 예를 들어 책 제목이 20,000 Leagues Under the Sea 이면 f_string() 은 Here is the title: 20,000 Leagues Under the Sea 를 반환합니다.

- 현재 날짜가 2025년 9월 18일이라고 가정하면, 다음 f_string()

f_string("Today's date: the {d = today(); format_date(d, 'd')} of {format_date(d, 'MMMM')}, {format_date(d, 'yyyy')}")

은 Today’s date: the 18 of September, 2025 문자열을 반환합니다. 첫 번째 { ... } 그룹에서 식 목록(대입 후 if 문)을 사용해 오늘 날짜를 지역 변수에 할당하는 점에 유의하십시오.

- 책이 Foo 라는 시리즈의 3권이고 전체가 5권이라면, 다음 템플릿

program:
if $series then
series_count = book_count('series:"""=' & $series & '"""', 0);
return f_string("{$series}, book {$series_index} of {series_count}")
fi;
return 'This book is not in a series'

은 Foo, book 3 of 5 를 반환합니다.

finish_formatting

class calibre.utils.formatter_functions.BuiltinFinishFormatting[소스]

finish_formatting(value, format, prefix, suffix) – 템플릿 {series_index:05.2f| - |- } 와 같은 방식으로 value 에 format, prefix, suffix 를 적용합니다. 템플릿에서 쓰는 마무리 형식을 함수 형태로 사용할 때 유용합니다.

format_date

class calibre.utils.formatter_functions.BuiltinFormatDate[소스]

format_date(value, format_string) – 날짜 문자열이어야 하는 value 를 format_string 으로 형식화해 문자열로 반환합니다. 날짜는 ISO 형식일 때 가장 안전합니다. 다른 날짜 형식을 사용하면 실제 날짜 값을 모호하지 않게 판단할 수 없어 오류가 날 수 있습니다. 또한 format_date_field() 함수가 더 빠르고 더 신뢰할 수 있다는 점에 유의하십시오.

형식 코드는 다음과 같습니다:

- d    : 앞에 0이 없는 날짜 숫자(1~31)

- dd   : 앞에 0이 붙는 날짜 숫자(01~31)

- ddd  : 지역화된 요일 약칭

- dddd : 지역화된 요일 전체 이름

- M    : 앞에 0이 없는 월 숫자(1~12)

- MM   : 앞에 0이 붙는 월 숫자(01~12)

- MMM  : 지역화된 월 약칭

- MMMM : 지역화된 월 전체 이름

- yy   : 두 자리 연도(00~99)

- yyyy : 네 자리 연도

- h    : 앞에 0이 없는 시(0~11 또는 0~23, am/pm 설정에 따라 다름)

- hh   : 앞에 0이 붙는 시(00~11 또는 00~23, am/pm 설정에 따라 다름)

- m    : 앞에 0이 없는 분(0~59)

- mm   : 앞에 0이 붙는 분(00~59)

- s    : 앞에 0이 없는 초(0~59)

- ss   : 앞에 0이 붙는 초(00~59)

- ap   : 24시간제 대신 12시간제를 사용하며, ap 는 지역화된 소문자 am/pm 문자열로 바뀝니다

- AP   : 24시간제 대신 12시간제를 사용하며, AP 는 지역화된 대문자 AM/PM 문자열로 바뀝니다

- aP   : 24시간제 대신 12시간제를 사용하며, aP 는 지역화된 AM/PM 문자열로 바뀝니다

- Ap   : 24시간제 대신 12시간제를 사용하며, Ap 는 지역화된 AM/PM 문자열로 바뀝니다

- iso  : 시간과 시간대를 포함한 날짜입니다. 이 형식만 단독으로 사용할 수 있습니다

- to_number   : 날짜와 시간을 부동소수점 숫자(timestamp)로 변환합니다

- from_number : 부동소수점 숫자(timestamp)를 ISO 형식 날짜로 변환합니다. 다른 날짜 형식을 원하면 from_number 뒤에 콜론(:)과 원하는 형식 문자열을 덧붙이십시오. 예:

format_date(val, 'from_number:MMM dd yyyy')

형식화하려는 날짜에 지역화된 월 이름이 들어 있으면 예상치 못한 결과가 나올 수 있습니다. 날짜 형식에 MMMM 를 포함하도록 바꾼 경우 이런 일이 생길 수 있습니다. format_date_field() 를 사용하면 이 문제를 피할 수 있습니다.

format_date_field

class calibre.utils.formatter_functions.BuiltinFormatDateField[소스]

format_date_field(field_name, format_string) – 날짜 필드(표준 또는 사용자 정의)의 조회 이름인 field_name 에 들어 있는 값을 형식화합니다. 자세한 형식 코드는 format_date() 를 참조하십시오.

format_duration

class calibre.utils.formatter_functions.BuiltinFormatDuration[소스]

format_duration(value, template, [largest_unit]) – 초 단위 숫자인 value 를 주, 일, 시, 분, 초를 보여 주는 문자열로 형식화합니다. 값이 실수이면 가장 가까운 정수로 반올림합니다. 템플릿은 [w], [d], [h], [m], [s] 선택자를 사용하며 선택자 사이에는 임의의 텍스트를 넣을 수 있습니다. largest_unit 를 사용해 가장 큰 단위를 제한할 수 있습니다.

format_number

class calibre.utils.formatter_functions.BuiltinFormatNumber[소스]

format_number(value, template) – value 를 숫자로 해석하고 {0:5.2f}, {0:,d}, ${0:5,.2f} 같은 Python 서식 템플릿을 사용해 형식화합니다.

human_readable

class calibre.utils.formatter_functions.BuiltinHumanReadable[소스]

human_readable(value) – value 를 숫자로 간주하고 KB, MB, GB 등의 형식으로 표현한 문자열을 반환합니다.

rating_to_stars

class calibre.utils.formatter_functions.BuiltinRatingToStars[소스]

rating_to_stars(value, use_half_stars) – value 를 별(★) 문자 문자열로 반환합니다. 값은 0 에서 5 사이의 숫자여야 합니다. 사용자 정의 평점 열에서 소수 값을 반별 문자로 표현하려면 use_half_stars 를 1 로 설정하십시오.

관계 연산

cmp

class calibre.utils.formatter_functions.BuiltinCmp[소스]

cmp(value, y, lt, eq, gt) – value 와 y 를 둘 다 숫자로 변환한 뒤 비교합니다. value <# y 이면 lt, value ==# y 이면 eq, 그 외에는 gt 를 반환합니다. 보통 비교 연산자로 대체할 수 있습니다.

first_matching_cmp

class calibre.utils.formatter_functions.BuiltinFirstMatchingCmp[소스]

first_matching_cmp(val, [ cmp, result, ]* else_result) – val < cmp 비교를 순서대로 수행해 처음으로 성공한 비교에 연결된 result 를 반환합니다. 일치하는 비교가 없으면 else_result 를 반환합니다.

strcmp

class calibre.utils.formatter_functions.BuiltinStrcmp[소스]

strcmp(x, y, lt, eq, gt) – x 와 y 를 대소문자 구분 없이 사전식으로 비교합니다. x < y 이면 lt, x == y 이면 eq, 그 외에는 gt 를 반환합니다. 많은 경우 비교 연산자로 대체할 수 있습니다.

strcmpcase

class calibre.utils.formatter_functions.BuiltinStrcmpcase[소스]

strcmpcase(x, y, lt, eq, gt) – x 와 y 를 대소문자를 구분해 사전식으로 비교합니다. x < y 이면 lt 를, x == y 이면 eq 를, 그 외에는 gt 를 반환합니다.

참고: 이는 calibre 가 기본적으로 사용하는 비교 방식이 아닙니다. 예를 들어 사전식 비교 연산자(==, >, < 등)는 이렇게 동작하지 않습니다. 이 함수는 예상치 못한 결과를 낼 수 있으므로 가능하면 strcmp() 를 사용하십시오.

기타

arguments

class calibre.utils.formatter_functions.BuiltinArguments[소스]

arguments(id[=expression] [, id[=expression]]*) – 저장된 템플릿에서 호출 시 전달된 인수를 가져오는 데 사용합니다. 전달된 이름으로 지역 변수를 선언하고 초기화합니다.

assign

class calibre.utils.formatter_functions.BuiltinAssign[소스]

assign(id, value) – value 를 id 에 할당한 뒤 value 를 반환합니다. id 는 식이 아니라 식별자여야 합니다. 대부분의 경우 = 연산자로 대체할 수 있습니다.

globals

class calibre.utils.formatter_functions.BuiltinGlobals[소스]

globals(id[=expression] [, id[=expression]]*) – 포매터에 전달할 수 있는 “전역 변수” 를 가져옵니다. id 는 전역 변수 이름입니다. 또한 해당 이름의 지역 변수를 선언하고 초기화합니다.

is_dark_mode

class calibre.utils.formatter_functions.BuiltinIsDarkMode[소스]

is_dark_mode() – calibre 가 다크 모드로 실행 중이면 '1' 을, 아니면 ``’’``(빈 문자열)을 반환합니다. 고급 색상 및 아이콘 규칙에서 모드별 다른 색상/아이콘을 선택하는 데 사용할 수 있습니다.

print

class calibre.utils.formatter_functions.BuiltinPrint[소스]

print(a [, b]*) – 인수들을 표준 출력으로 출력합니다. calibre 를 명령행(calibre-debug -g)에서 시작하지 않았다면 출력은 보이지 않습니다. print 함수는 항상 빈 문자열을 반환합니다.

set_globals

class calibre.utils.formatter_functions.BuiltinSetGlobals[소스]

set_globals(id[=expression] [, id[=expression]]*) – 포매터에 전달할 수 있는 전역 / 변수 를 설정합니다. 전역 변수 이름은 전달한 id 를 사용합니다. id 의 값은 변수 값으로 사용됩니다.

날짜 함수

date_arithmetic

class calibre.utils.formatter_functions.BuiltinDateArithmetic[소스]

date_arithmetic(value, calc_spec, fmt) – calc_spec 을 사용해 value 로부터 새 날짜를 계산합니다. 선택적 fmt 에 따라 새 날짜를 형식화해 반환합니다. fmt 를 지정하지 않으면 결과는 ISO 형식이 됩니다.

days_between

class calibre.utils.formatter_functions.BuiltinDaysBetween[소스]

days_between(date1, date2) – date1 과 date2 사이의 일 수를 반환합니다. date1 이 date2 보다 크면 양수, 그렇지 않으면 음수입니다. 날짜를 해석할 수 없으면 빈 문자열을 반환합니다.

today

class calibre.utils.formatter_functions.BuiltinToday[소스]

today() – 오늘(현재 시각)의 날짜+시간 문자열을 반환합니다. 이 값은 format_date 나 days_between 에 사용하도록 설계되었지만 일반 문자열처럼 조작할 수도 있습니다. 날짜는 ISO 형식입니다.

대소문자 변경

capitalize

class calibre.utils.formatter_functions.BuiltinCapitalize[소스]

capitalize(value) – value 의 첫 글자는 대문자로, 나머지는 소문자로 바꿔 반환합니다.

lowercase

class calibre.utils.formatter_functions.BuiltinLowercase[소스]

lowercase(value) – ``value``를 소문자로 반환합니다.

titlecase

class calibre.utils.formatter_functions.BuiltinTitlecase[소스]

titlecase(value) – ``value``를 제목 형식 대소문자로 반환합니다.

uppercase

class calibre.utils.formatter_functions.BuiltinUppercase[소스]

uppercase(value) – ``value``를 대문자로 반환합니다.

데이터베이스 함수

annotation_count

class calibre.utils.formatter_functions.BuiltinAnnotationCount[소스]

annotation_count() – 현재 책에 연결된 모든 종류의 주석(annotation) 총개수를 반환합니다. 이 함수는 GUI 와 콘텐츠 서버에서만 동작합니다.

approximate_formats

class calibre.utils.formatter_functions.BuiltinApproximateFormats[소스]

approximate_formats() – 책과 연결된 형식의 콤마 구분 목록을 반환합니다. 이 목록은 파일 시스템이 아니라 calibre 데이터베이스에서 가져오므로 실제 파일이 존재한다고 보장되지는 않습니다.

book_count

class calibre.utils.formatter_functions.BuiltinBookCount[소스]

book_count(query, use_vl) – query 로 검색해 찾은 책 수를 반환합니다. use_vl 이 0 이면 가상 라이브러리를 무시합니다. 이 함수와 짝인 book_values() 는 composite 열에서 사용할 수 있지만 느릴 수 있습니다.

book_values

class calibre.utils.formatter_functions.BuiltinBookValues[소스]

book_values(column, query, sep, use_vl) – query 로 찾은 책들에서 열 column``(조회 이름)에 들어 있는 고유 값 목록을 ``sep 로 구분해 반환합니다. use_vl 이 0 이면 가상 라이브러리를 무시합니다.

extra_file_modtime

class calibre.utils.formatter_functions.BuiltinExtraFileModtime[소스]

extra_file_modtime(file_name, format_string) – 책의 data/ 폴더에 file_name 추가 파일이 있으면 그 수정 시각을 반환하고 없으면 -1 을 반환합니다. 수정 시각은 format_string 에 따라 형식화됩니다.

extra_file_names

class calibre.utils.formatter_functions.BuiltinExtraFileNames[소스]

extra_file_names(sep [, pattern]) – 책의 data/ 폴더 안에 있는 추가 파일 목록을 sep 로 구분해 반환합니다. 선택적 정규식 pattern 을 주면 그 조건에 맞는 파일만 포함합니다.

extra_file_size

class calibre.utils.formatter_functions.BuiltinExtraFileSize[소스]

extra_file_size(file_name) – 책의 data/ 폴더에 file_name 추가 파일이 있으면 그 크기를 바이트 단위로 반환하고, 없으면 -1 을 반환합니다.

formats_modtimes

class calibre.utils.formatter_functions.BuiltinFormatsModtimes[소스]

formats_modtimes(date_format_string) – 책 형식들의 수정 시각을 나타내는 FMT:DATE 형식 항목의 콤마 구분 목록을 반환합니다. date_format_string 인수는 날짜 형식을 지정합니다.

formats_path_segments

class calibre.utils.formatter_functions.BuiltinFormatsPathSegments[소스]

formats_path_segments(with_author, with_title, with_format, with_ext, sep) – calibre 라이브러리 안에서 책 형식 경로의 각 부분을 sep 로 구분해 반환합니다. sep 는 보통 파일 시스템에 사용되지 않는 문자열이어야 합니다.

formats_paths

class calibre.utils.formatter_functions.BuiltinFormatsPaths[소스]

formats_paths([separator]) – 책 형식의 전체 경로를 나타내는 FMT:PATH 형식 항목의 separator 구분 목록을 반환합니다. separator 인수는 선택 사항이며, 지정하지 않으면 쉼표가 사용됩니다.

formats_sizes

class calibre.utils.formatter_functions.BuiltinFormatsSizes[소스]

formats_sizes() – 책 형식의 크기를 바이트 단위로 나타내는 FMT:SIZE 형식 항목의 콤마 구분 목록을 반환합니다. 특정 형식의 크기를 얻으려면 select() 함수를 사용할 수 있습니다.

get_link

class calibre.utils.formatter_functions.BuiltinGetLink[소스]

get_link(field_name, field_value) – 값이 field_value 인 field_name 필드의 링크를 가져옵니다. 연결된 링크가 없으면 빈 문자열을 반환합니다. 예:

- 다음은 태그 Fiction 에 연결된 링크를 반환합니다:

get_link('tags', 'Fiction')

- 다음 템플릿은 책에 연결된 모든 태그의 링크를 value:link, ... 형식의 목록으로 만듭니다:

program:
ans = '';
for t in $tags:
l = get_link('tags', t);
if l then
ans = list_join(', ', ans, ',', t & ':' & get_link('tags', t), ',')
fi
rof;
ans

이 함수는 GUI 와 콘텐츠 서버에서만 동작합니다.

get_note

class calibre.utils.formatter_functions.BuiltinGetNote[소스]

get_note(field_name, field_value, plain_text) – 값이 field_value 인 field_name 필드의 노트를 가져옵니다. plain_text 가 비어 있으면 이미지를 포함한 노트 HTML 을 반환합니다. plain_text 가 비어 있지 않으면 일반 텍스트를 반환합니다.

has_extra_files

class calibre.utils.formatter_functions.BuiltinHasExtraFiles[소스]

has_extra_files([pattern]) – 추가 파일 개수를 반환하고, 없으면 ‘’(빈 문자열)을 반환합니다. 선택적 인수 ``pattern``(정규식)을 주면 그 정규식과 일치하는 파일만 대상으로 합니다.

has_note

class calibre.utils.formatter_functions.BuiltinHasNote[소스]

has_note(field_name, field_value). 필드에 노트가 있는지 확인합니다. 이 함수에는 두 가지 변형이 있습니다: field_value 가 빈 문자열이 아니면 해당 값에 노트가 있는지 확인하고, 빈 문자열이면 필드 자체에 노트가 있는지 확인합니다.

reading_progress

class calibre.utils.formatter_functions.BuiltinReadingProgress[소스]

reading_progress(book_id, [user, output_fmt, which, fmt]) – 지정한 출력 형식으로 읽기 진행률을 반환합니다. user 매개변수는 기본적으로 모든 사용자와 일치합니다. local 값은 calibre 전자책 뷰어의 읽기 진행률을 뜻하고, _ 는 콘텐츠 서버 뷰어의 익명 사용자 진행률을 뜻합니다. 그 밖의 값은 콘텐츠 서버에서 사용하는 해당 사용자 이름과 일치합니다.

output_fmt 매개변수는 이 함수가 반환하는 텍스트 형식을 제어합니다. 사용할 수 있는 값은 세 가지입니다:

- page_count - 기본값. 읽은 페이지 / 전체 페이지 를 출력합니다. 페이지 수 계산이 활성화되어 있지 않으면 대신 읽은 비율을 출력합니다

- percent - 읽은 비율을 출력합니다

- pos_frac - 0과 1 사이의 분수값을 출력합니다

which 매개변수는 지정한 user 에 대해 어떤 읽기 진행률 기록을 선택할지 제어합니다. 사용자를 지정하지 않았거나 책을 여러 형식 또는 여러 장치에서 읽은 경우에는 기록이 둘 이상일 수 있습니다. 사용할 수 있는 값은 두 가지입니다:

- most_recent - 이 책을 가장 최근에 읽은 기록의 진행률(기본값)

- furthest - 일치하는 기록 가운데 가장 멀리 읽은 진행률

fmt 매개변수는 어떤 책 형식을 사용할지 제어합니다. 기본값은 모든 형식의 기록을 반환하는 것이며, 실제로 사용할 특정 기록은 which 매개변수로 선택됩니다.

예:

{id:reading_progress()} -- 이 책의 가장 최근 읽기 세션에 대한
읽은 페이지 / 전체 페이지 형태의 진행률
{id:reading_progress(,percent)} -- 위와 같지만 백분율로 출력
{id:reading_progress(,pos_frac,furthest)} -- 위와 같지만 분수값으로 출력하고
이 책에서 가장 많이 읽은 기록을 사용
{id:reading_progress(bob,pos_frac,furthest,EPUB)} -- 사용자 "bob" 의 "EPUB" 형식에 대한 진행률

메타데이터에서 값 가져오기

author_links

class calibre.utils.formatter_functions.BuiltinAuthorLinks[소스]

author_links(val_separator, pair_separator) – 저자와 해당 저자의 링크 값을 포함한 문자열을 반환합니다 형식은 author1 val_separator author1_link pair_separator author2 val_separator author2_link ... 입니다.

author_sorts

class calibre.utils.formatter_functions.BuiltinAuthorSorts[소스]

author_sorts(val_separator) – 책의 저자들에 대한 정렬값 목록을 포함한 문자열을 반환합니다. 사용되는 정렬값은 저자 메타데이터 정보의 값이며 저자 이름 문자열과 다를 수 있습니다.

booksize

class calibre.utils.formatter_functions.BuiltinBooksize[소스]

booksize() – calibre size 필드 값을 반환합니다. 책에 형식이 없으면 ‘’ 를 반환합니다. / / 이 함수는 GUI 에서만 동작합니다. 이 값을 디스크에 저장 또는 장치로 보내기에 사용하려면 저장된 템플릿이나 다른 데이터 소스를 사용하십시오.

connected_device_name

class calibre.utils.formatter_functions.BuiltinConnectedDeviceName[소스]

connected_device_name(storage_location_key) – 장치가 연결되어 있으면 장치 이름을, 아니면 빈 문자열을 반환합니다. 장치의 각 저장 위치는 고유한 장치 이름을 가질 수 있습니다. storage_location_key 는 해당 저장 위치를 지정합니다.

connected_device_uuid

class calibre.utils.formatter_functions.BuiltinConnectedDeviceUUID[소스]

connected_device_uuid(storage_location_key) – 장치가 연결되어 있으면 장치 uuid(고유 ID)를, 아니면 빈 문자열을 반환합니다. 장치의 각 저장 위치는 서로 다른 uuid 를 가집니다.

current_library_name

class calibre.utils.formatter_functions.BuiltinCurrentLibraryName[소스]

current_library_name() – 현재 calibre 라이브러리 경로의 마지막 이름을 반환합니다.

current_library_path

class calibre.utils.formatter_functions.BuiltinCurrentLibraryPath[소스]

current_library_path() – 현재 calibre 라이브러리의 전체 경로를 반환합니다.

current_virtual_library_name

class calibre.utils.formatter_functions.BuiltinCurrentVirtualLibraryName[소스]

current_virtual_library_name() – 현재 가상 라이브러리가 있으면 그 이름을, 없으면 빈 문자열을 반환합니다. 라이브러리 이름의 대소문자는 유지됩니다. 예[CODEprogram: current_virtual_library_name([/CODE이 함수는 GUI 에서만 동작합니다.

field

class calibre.utils.formatter_functions.BuiltinField[소스]

field(lookup_name) – 조회 이름이 lookup_name 인 메타데이터 필드 값을 반환합니다. 함수 대신 $ 접두사를 사용할 수도 있습니다. 예: $tags.

has_cover

class calibre.utils.formatter_functions.BuiltinHasCover[소스]

has_cover() – 책에 표지가 있으면 'Yes' 를, 없으면 빈 문자열을 반환합니다.

is_marked

class calibre.utils.formatter_functions.BuiltinIsMarked[소스]

is_marked() – 책이 calibre 에서 marked 상태인지 확인합니다. 표시되어 있으면 'true'``(소문자) 또는 이름 있는 표시의 콤마 구분 목록을 반환합니다. 표시되지 않았으면 ``'' 를 반환합니다.

language_codes

class calibre.utils.formatter_functions.BuiltinLanguageCodes[소스]

language_codes(lang_strings) – lang_strings 로 전달된 언어 이름의 언어 코드 [https://www.loc.gov/standards/iso639-2/php/code_list.php] 를 반환합니다. 문자열은 현재 로캘의 언어여야 합니다. lang_strings 는 콤마 구분 목록입니다.

language_strings

class calibre.utils.formatter_functions.BuiltinLanguageStrings[소스]

language_strings(value, localize) – value 로 전달된 언어 코드의 언어 이름을 반환합니다(이름과 코드는 여기 참조 [https://www.loc.gov/standards/iso639-2/php/code_list.php]). 예: {languages:language_strings()}. localize 가 0 이면 영어 이름을, 0 이 아니면 현재 로캘 언어의 이름을 반환합니다. lang_codes 는 콤마 구분 목록입니다.

ondevice

class calibre.utils.formatter_functions.BuiltinOndevice[소스]

ondevice() – ondevice 가 설정되어 있으면 문자열 'Yes' 를, 그렇지 않으면 빈 문자열을 반환합니다. 이 함수는 GUI 에서만 동작합니다.

raw_field

class calibre.utils.formatter_functions.BuiltinRawField[소스]

raw_field(lookup_name [, optional_default]) – 형식 적용 없이 lookup_name 이 가리키는 메타데이터 필드 값을 반환합니다. 필드가 없으면 선택적 두 번째 인수 optional_default 를 평가해 반환합니다.

raw_list

class calibre.utils.formatter_functions.BuiltinRawList[소스]

raw_list(lookup_name, separator) – 형식 지정이나 정렬을 적용하지 않고 lookup_name 이 가리키는 메타데이터 목록을 반환하며 항목은 separator 로 구분합니다.

series_sort

class calibre.utils.formatter_functions.BuiltinSeriesSort[소스]

series_sort() – 시리즈 정렬 값을 반환합니다.

user_categories

class calibre.utils.formatter_functions.BuiltinUserCategories[소스]

user_categories() – 이 책이 포함된 사용자 범주의 콤마 구분 목록을 반환합니다. 이 함수는 GUI 에서만 동작합니다.

virtual_libraries

class calibre.utils.formatter_functions.BuiltinVirtualLibraries[소스]

virtual_libraries() – 이 책이 포함된 가상 라이브러리의 콤마 구분 목록을 반환합니다. 이 함수는 GUI 에서만 동작합니다.

목록 조작

list_count

class calibre.utils.formatter_functions.BuiltinCount[소스]

list_count(value, separator) – 값을 separator 로 구분된 항목 목록으로 해석하고 그 개수를 반환합니다. 대부분의 목록은 쉼표를 구분자로 사용하지만 authors 는 & 를 사용합니다.

list_count_field

class calibre.utils.formatter_functions.BuiltinFieldListCount[소스]

list_count_field(lookup_name)– 조회 이름이 lookup_name 인 필드의 항목 수를 반환합니다. 해당 필드는 authors 나 tags 처럼 다중 값 필드여야 하며, 그렇지 않으면 예외가 발생합니다.

list_count_matching

class calibre.utils.formatter_functions.BuiltinListCountMatching[소스]

list_count_matching(value, pattern, separator) – value 를 separator 로 구분된 항목 목록으로 해석하고 정규식 pattern 과 일치하는 항목 수를 반환합니다.

list_difference

class calibre.utils.formatter_functions.BuiltinListDifference[소스]

list_difference(list1, list2, separator) – 대소문자를 구분하지 않는 비교를 사용해 list1 에서 list2 에 있는 항목을 제거한 목록을 반환합니다.

list_equals

class calibre.utils.formatter_functions.BuiltinListEquals[소스]

list_equals(list1, sep1, list2, sep2, yes_val, no_val) – list1 과 list2 에 동일한 항목이 들어 있으면 yes_val 을, 아니면 no_val 을 반환합니다. 각 목록은 각각 sep1 과 sep2 로 분리해 항목을 구합니다.

list_intersection

class calibre.utils.formatter_functions.BuiltinListIntersection[소스]

list_intersection(list1, list2, separator) – 대소문자를 구분하지 않는 비교를 사용해 list1 과 list2 에 공통으로 있는 항목만 남긴 목록을 반환합니다.

list_join

class calibre.utils.formatter_functions.BuiltinListJoin[소스]

list_join(with_separator, list1, separator1 [, list2, separator2]*) – 원본 목록들(list1 등)의 항목을 결합해 새 목록을 만듭니다. 결과 목록의 항목 사이에는 with_separator 가 사용됩니다.

list_re

class calibre.utils.formatter_functions.BuiltinListRe[소스]

list_re(src_list, separator, include_re, opt_replace) – 먼저 src_list 를 separator 문자로 나눠 목록을 만든 뒤 각 항목이 include_re 와 일치하는지 검사해 새 목록을 만듭니다. opt_replace 를 지정하면 일치한 항목에 치환을 적용합니다.

list_re_group

class calibre.utils.formatter_functions.BuiltinListReGroup[소스]

list_re_group(src_list, separator, include_re, search_re [,template_for_group]*) – list_re() 와 비슷하지만 치환은 선택 사항이 아닙니다. 치환 시 re_group(item, search_re, template ...) 를 사용합니다.

list_remove_duplicates

class calibre.utils.formatter_functions.BuiltinListRemoveDuplicates[소스]

list_remove_duplicates(list, separator) – list 에서 중복 항목을 제거한 목록을 반환합니다. 항목이 대소문자만 다르면 마지막 항목이 남습니다. 항목 구분자는 separator 입니다.

list_sort

class calibre.utils.formatter_functions.BuiltinListSort[소스]

list_sort(value, direction, separator) – 대소문자를 구분하지 않는 사전식 정렬로 value 를 정렬해 반환합니다. direction 이 0 이면 오름차순, 그 외에는 내림차순입니다.

list_split

class calibre.utils.formatter_functions.BuiltinListSplit[소스]

list_split(list_val, sep, id_prefix) – list_val 을 sep 로 분리해 각각의 값을 별도 값으로 나눈 다음 각 값을 위치 번호 N 을 붙인 id_prefix_N 형식의 지역 변수에 할당합니다.

list_union

class calibre.utils.formatter_functions.BuiltinListUnion[소스]

list_union(list1, list2, separator) – list1 과 list2 의 항목을 합쳐 새 목록을 만들고 대소문자를 구분하지 않는 비교로 중복 항목을 제거합니다. 대소문자만 다른 경우 마지막 값을 사용합니다.

range

class calibre.utils.formatter_functions.BuiltinRange[소스]

range(start, stop, step, limit) – start, stop, step 으로 지정한 범위를 순회해 생성한 숫자 목록을 반환하며 최대 길이는 limit 입니다.

subitems

class calibre.utils.formatter_functions.BuiltinSubitems[소스]

subitems(value, start_index, end_index) – 장르처럼 계층 구조를 가진 태그형 항목 목록을 분해하는 함수입니다. value 를 콤마 구분 목록으로 해석하고 각 계층 항목에서 지정한 범위의 부분만 추출합니다.

sublist

class calibre.utils.formatter_functions.BuiltinSublist[소스]

sublist(value, start_index, end_index, separator) – value 를 separator 로 구분된 항목 목록으로 해석하고, start_index 부터 end_index 까지의 항목으로 이루어진 새 목록을 반환합니다. 첫 번째 항목의 번호는 0입니다. 인덱스가 음수이면 목록의 끝에서부터 셉니다. 특별히 end_index 가 0이면 목록 길이로 간주합니다.

태그 열(쉼표로 구분)이 “A, B, C” 라고 가정하면 예시는 다음과 같습니다:

- {tags:sublist(0,1,\,)} 는 “A” 를 반환합니다.

- {tags:sublist(-1,0,\,)} 는 “C” 를 반환합니다.

- {tags:sublist(0,-1,\,)} 는 “A, B” 를 반환합니다.

목록 조회

identifier_in_list

class calibre.utils.formatter_functions.BuiltinIdentifierInList[소스]

identifier_in_list(val, id_name [, found_val, not_found_val]) – val 을 콤마로 구분된 식별자 목록으로 처리합니다. 식별자 형식은 id_name:value 입니다. id_name 은 찾을 식별자 이름입니다. 찾으면 found_val 을, 찾지 못하면 not_found_val 을 반환합니다.

list_contains

class calibre.utils.formatter_functions.BuiltinInList[소스]

list_contains(value, separator, [ pattern, found_val, ]* not_found_val) – value 를 separator 로 구분된 항목 목록으로 해석하고 각 항목을 정규식 pattern 과 비교합니다. 어떤 항목이라도 일치하면 해당 found_val 을 반환하고, 일치하는 것이 없으면 not_found_val 을 반환합니다.

list_item

class calibre.utils.formatter_functions.BuiltinListitem[소스]

list_item(value, index, separator) – value 를 separator 로 구분된 항목 목록으로 해석하여 index 번째 항목을 반환합니다. 첫 항목의 인덱스는 0 이고 마지막 항목은 -1 입니다.

select

class calibre.utils.formatter_functions.BuiltinSelect[소스]

select(value, key) – value 를 각 항목이 id:id_value 형식인 콤마 구분 목록( calibre identifier 형식 )으로 해석합니다. 함수는 key 와 일치하는 첫 쌍을 찾아 그 값을 반환합니다.

str_in_list

class calibre.utils.formatter_functions.BuiltinStrInList[소스]

str_in_list(value, separator, [ string, found_val, ]+ not_found_val) – value 를 separator 로 구분된 항목 목록으로 해석한 뒤 각 항목과 string 을 비교합니다. 일치하면 대응하는 found_val 을 반환하고, 일치하는 것이 없으면 not_found_val 을 반환합니다.

문자열 조작

character

class calibre.utils.formatter_functions.BuiltinCharacter[소스]

character(character_name) – character_name 으로 지정한 문자를 반환합니다. 예를 들어 character('newline') 는 줄바꿈 문자('')를 반환합니다. 지원되는 이름에는 newline 등이 있습니다.

check_yes_no

class calibre.utils.formatter_functions.BuiltinCheckYesNo[소스]

check_yes_no(field_name, is_undefined, is_false, is_true) – 조회 이름이 field_name 인 예/아니오 필드의 값이 매개변수로 지정한 값 중 하나인지 검사하고 일치하면 'Yes' 를, 일치하지 않으면 빈 문자열을 반환합니다. 검사하려는 조건에 따라 is_undefined, is_false, is_true 를 1(숫자)로 설정하고, 검사하지 않을 조건은 0으로 설정하십시오.

예: check_yes_no("#bool", 1, 0, 1) 은 예/아니오 필드 #bool 이 True 이거나 정의되지 않은 경우(True 도 False 도 아님) 'Yes' 를 반환합니다.

is_undefined, is_false, is_true 중 둘 이상을 동시에 1로 설정할 수 있습니다.

contains

class calibre.utils.formatter_functions.BuiltinContains[소스]

contains(value, pattern, text_if_match, text_if_not_match) – 값이 정규식 pattern 과 일치하는지 검사합니다. 일치하면 text_if_match 를, 그렇지 않으면 text_if_not_match 를 반환합니다.

field_exists

class calibre.utils.formatter_functions.BuiltinFieldExists[소스]

field_exists(lookup_name) – 조회 이름이 lookup_name 인 필드(열)가 존재하는지 확인하고, 존재하면 '1' 을, 없으면 빈 문자열을 반환합니다.

ifempty

class calibre.utils.formatter_functions.BuiltinIfempty[소스]

ifempty(value, text_if_empty) – value 가 비어 있지 않으면 그 값을 반환하고, 비어 있으면 text_if_empty 를 반환합니다.

re

class calibre.utils.formatter_functions.BuiltinRe[소스]

re(value, pattern, replacement) – 정규식을 적용한 뒤의 value 를 반환합니다. 값 안의 pattern 에 해당하는 모든 부분을 replacement 로 바꿉니다. 템플릿 언어의 정규식은 기본적으로 대소문자를 구분하지 않습니다.

re_group

class calibre.utils.formatter_functions.BuiltinReGroup[소스]

re_group(value, pattern [, template_for_group]*) – 정규식 pattern 을 value 에 적용해 만들어진 문자열을 반환하고 일치한 각 항목은 해당 그룹에 대한 템플릿 결과로 치환합니다.

shorten

class calibre.utils.formatter_functions.BuiltinShorten[소스]

shorten(value, left_chars, middle_text, right_chars) – value 의 축약형을 반환합니다. 앞부분에서 left_chars 개 문자, 그 뒤 middle_text, 마지막으로 뒷부분에서 right_chars 개 문자를 붙입니다.

strcat

class calibre.utils.formatter_functions.BuiltinStrcat[소스]

strcat(a [, b]*) – 모든 인수를 이어 붙여 만든 문자열을 반환합니다. 인수 개수는 제한이 없습니다. 대부분의 경우 이 함수 대신 & 연산자를 사용할 수 있습니다.

strcat_max

class calibre.utils.formatter_functions.BuiltinStrcatMax[소스]

strcat_max(max, string1 [, prefix2, string2]*) – 인수들을 이어 붙여 만든 문자열을 반환합니다. 반환값은 string1 으로 시작합니다. 이후 prefix, string 쌍에서 만든 문자열은 전체 길이가 max 를 넘지 않을 때만 추가됩니다.

strlen

class calibre.utils.formatter_functions.BuiltinStrlen[소스]

strlen(value) – 문자열 ``value``의 길이를 반환합니다.

substr

class calibre.utils.formatter_functions.BuiltinSubstr[소스]

substr(value, start, end) – value 의 start 번째 문자부터 end 번째 문자까지를 반환합니다. 첫 문자의 인덱스는 0 입니다. end 가 음수이면 문자열 끝에서부터의 위치를 뜻합니다.

swap_around_articles

class calibre.utils.formatter_functions.BuiltinSwapAroundArticles[소스]

swap_around_articles(value, separator) – value 의 관사를 끝으로 옮겨 세미콜론으로 구분해 반환합니다. value 가 목록일 수도 있으며 그 경우 각 항목을 개별 처리합니다.

swap_around_comma

class calibre.utils.formatter_functions.BuiltinSwapAroundComma[소스]

swap_around_comma(value) – B, A 형식의 value 를 받아 A B 를 반환합니다. 성, 이름 형식(LN, FN)의 이름을 이름 성(FN LN) 형식으로 바꿀 때 유용합니다. value 에 쉼표가 없으면 값을 그대로 반환합니다.

test

class calibre.utils.formatter_functions.BuiltinTest[소스]

test(value, text_if_not_empty, text_if_empty) – 값이 비어 있지 않으면 text_if_not_empty 를, 비어 있으면 text_if_empty 를 반환합니다.

transliterate

class calibre.utils.formatter_functions.BuiltinTransliterate[소스]

transliterate(value) – value 의 단어 소리를 근사해 라틴 문자 알파벳 문자열로 반환합니다. 예를 들어 value 가 Фёдор Миха́йлович Достоевский 이면 이 함수는 Fiodor Mikhailovich Dostoievskii 을 반환합니다.

불리언

and

class calibre.utils.formatter_functions.BuiltinAnd[소스]

and(value [, value]*) – 모든 값이 비어 있지 않으면 문자열 '1' 을, 그렇지 않으면 빈 문자열을 반환합니다. 인수 개수는 제한이 없습니다. 대부분의 경우 && 연산자로 대체할 수 있습니다.

not

class calibre.utils.formatter_functions.BuiltinNot[소스]

not(value) – 값이 비어 있으면 문자열 '1' 을, 그렇지 않으면 빈 문자열을 반환합니다. 보통 단항 not(!) 연산자로 대체할 수 있습니다.

or

class calibre.utils.formatter_functions.BuiltinOr[소스]

or(value [, value]*) – 값 중 하나라도 비어 있지 않으면 문자열 '1' 을, 모두 비어 있으면 빈 문자열을 반환합니다. 인수 개수는 제한이 없습니다. 보통 || 연산자로 대체할 수 있습니다.

산술

add

class calibre.utils.formatter_functions.BuiltinAdd[소스]

add(x [, y]*) – 인수들의 합을 반환합니다. 인수 중 숫자가 아닌 값이 있으면 예외를 발생시킵니다. 대부분의 경우 + 연산자로 대체할 수 있습니다.

ceiling

class calibre.utils.formatter_functions.BuiltinCeiling[소스]

ceiling(value) – value 이상인 가장 작은 정수를 반환합니다. value 가 숫자가 아니면 예외를 발생시킵니다.

divide

class calibre.utils.formatter_functions.BuiltinDivide[소스]

divide(x, y) – x / y 를 반환합니다. x 또는 y 가 숫자가 아니면 예외를 발생시킵니다. 보통 / 연산자로 대체할 수 있습니다.

floor

class calibre.utils.formatter_functions.BuiltinFloor[소스]

floor(value) – value 이하인 가장 큰 정수를 반환합니다. value 가 숫자가 아니면 예외를 발생시킵니다.

fractional_part

class calibre.utils.formatter_functions.BuiltinFractionalPart[소스]

fractional_part(value) – 값의 소수점 이하 부분을 반환합니다. 예를 들어 fractional_part(3.14) 는 0.14 를 반환합니다. value 가 숫자가 아니면 예외를 발생시킵니다.

mod

class calibre.utils.formatter_functions.BuiltinMod[소스]

mod(value, y) – value / y 의 나머지에 대해 floor 를 적용한 값을 반환합니다. value 또는 y 가 숫자가 아니면 예외를 발생시킵니다.

multiply

class calibre.utils.formatter_functions.BuiltinMultiply[소스]

multiply(x [, y]*) – 인수들의 곱을 반환합니다. 인수 중 숫자가 아닌 값이 있으면 예외를 발생시킵니다. 보통 * 연산자로 대체할 수 있습니다.

round

class calibre.utils.formatter_functions.BuiltinRound[소스]

round(value) – value 에 가장 가까운 정수를 반환합니다. value 가 숫자가 아니면 예외를 발생시킵니다.

subtract

class calibre.utils.formatter_functions.BuiltinSubtract[소스]

subtract(x, y) – x - y 를 반환합니다. x 또는 y 가 숫자가 아니면 예외를 발생시킵니다. 보통 - 연산자로 대체할 수 있습니다.

재귀

eval

class calibre.utils.formatter_functions.BuiltinEval[소스]

eval(string) – 문자열을 프로그램으로 평가하고 지역 변수를 전달합니다. 이를 통해 템플릿 처리기를 사용해 지역 변수로부터 복잡한 결과를 만들 수 있습니다. Template Program Mode [https://manual.calibre-ebook.com/template_lang.html#more-complex-programs-in-template-expressions-template-program-mode] 에서는 템플릿이 평가되기 전에 { 와 } 가 해석되므로 각각 [[ 와 ]] 를 사용해야 하며, 자동으로 변환됩니다. 또한 Template Program Mode 를 사용할 때는 이 함수의 인수 안에서 접두사/접미사(|prefix|suffix 구문)를 사용할 수 없습니다.

template

class calibre.utils.formatter_functions.BuiltinTemplate[소스]

template(x) – x 를 템플릿으로 평가합니다. 평가는 자체 컨텍스트에서 수행되므로 호출자와 템플릿 평가 사이에 변수는 공유되지 않습니다. General Program Mode 를 사용하지 않을 때는 { 와 } 가 특별한 문자이므로 각각 [[ 와 ]] 를 사용해야 하며, 자동으로 변환됩니다. 예를 들어 template('[[title_sort]]') 는 템플릿 {title_sort} 를 평가해 그 값을 반환합니다. 또한 Template Program Mode 를 사용할 때는 이 함수의 인수 안에서 접두사/접미사(|prefix|suffix 구문)를 사용할 수 없습니다.

API of the Metadata objects

The python implementation of the template functions is passed in a Metadata object. Knowing it’s API is useful if you want to define your own template functions.

class calibre.ebooks.metadata.book.base.Metadata(title, authors=('알 수 없음',), other=None, template_cache=None, formatter=None)[소스]

A class representing all the metadata for a book. The various standard metadata fields are available as attributes of this object. You can also stick arbitrary attributes onto this object.

Metadata from custom columns should be accessed via the get() method, passing in the lookup name for the column, for example: “#mytags”.

Use the is_null() method to test if a field is null.

This object also has functions to format fields into strings.

The list of standard metadata fields grows with time is in STANDARD_METADATA_FIELDS.

Please keep the method based API of this class to a minimum. Every method becomes a reserved field name.

is_null(field)[소스]

Return True if the value of field is null in this object.
‘null’ means it is unknown or evaluates to False. So a title of
_(‘Unknown’) is null or a language of ‘und’ is null.

Be careful with numeric fields since this will return True for zero as well as None.

Also returns True if the field does not exist.

deepcopy(class_generator=<function Metadata.<lambda>>)[소스]

Do not use this method unless you know what you are doing, if you want to create a simple clone of this object, use deepcopy_metadata() instead. Class_generator must be a function that returns an instance of Metadata or a subclass of it.

get_identifiers()[소스]

Return a copy of the identifiers dictionary.
The dict is small, and the penalty for using a reference where a copy is needed is large. Also, we don’t want any manipulations of the returned dict to show up in the book.

set_identifiers(identifiers)[소스]

Set all identifiers. Note that if you previously set ISBN, calling this method will delete it.

set_identifier(typ, val)[소스]

If val is empty, deletes identifier of type typ

standard_field_keys()[소스]

return a list of all possible keys, even if this book doesn’t have them

custom_field_keys()[소스]

return a list of the custom fields in this book

all_field_keys()[소스]

All field keys known by this instance, even if their value is None

metadata_for_field(key)[소스]

return metadata describing a standard or custom field.

all_non_none_fields()[소스]

Return a dictionary containing all non-None metadata fields, including the custom ones.

get_standard_metadata(field, make_copy)[소스]

return field metadata from the field if it is there. Otherwise return
None. field is the key name, not the label. Return a copy if requested, just in case the user wants to change values in the dict.

get_all_standard_metadata(make_copy)[소스]

return a dict containing all the standard field metadata associated with the book.

get_all_user_metadata(make_copy)[소스]

return a dict containing all the custom field metadata associated with the book.

get_user_metadata(field, make_copy)[소스]

return field metadata from the object if it is there. Otherwise return
None. field is the key name, not the label. Return a copy if requested, just in case the user wants to change values in the dict.

set_all_user_metadata(metadata)[소스]

store custom field metadata into the object. Field is the key name not the label

set_user_metadata(field, metadata)[소스]

store custom field metadata for one column into the object. Field is the key name not the label

remove_stale_user_metadata(other_mi)[소스]

Remove user metadata keys (custom column keys) if they don’t exist in ‘other_mi’, which must be a metadata object

template_to_attribute(other, ops)[소스]

Takes a list [(src,dest), (src,dest)], evaluates the template in the context of other, then copies the result to self[dest]. This is on a best-efforts basis. Some assignments can make no sense.

smart_update(other, replace_metadata=False)[소스]

Merge the information in other into self. In case of conflicts, the information in other takes precedence, unless the information in other is NULL.

format_field(key, series_with_index=True)[소스]

Returns the tuple (display_name, formatted_value)

to_html()[소스]

A HTML representation of this object.

calibre.ebooks.metadata.book.base.STANDARD_METADATA_FIELDS

The set of standard metadata fields.

'''
All fields must have a NULL value represented as None for simple types, an empty list/dictionary for complex types and (None, None) for cover_data
'''

SOCIAL_METADATA_FIELDS = frozenset((
'tags', # Ordered list
'rating', # A floating point number between 0 and 10
'comments', # A simple HTML enabled string
'series', # A simple string
'series_index', # A floating point number
# Of the form { scheme1:value1, scheme2:value2}
# For example: {'isbn':'123456789', 'doi':'xxxx', ... }
'identifiers',
))

'''
The list of names that convert to identifiers when in get and set.
'''

TOP_LEVEL_IDENTIFIERS = frozenset((
'isbn',
))

PUBLICATION_METADATA_FIELDS = frozenset((
'title', # title must never be None. Should be _('Unknown')
# Pseudo field that can be set, but if not set is auto generated
# from title and languages
'title_sort',
'authors', # Ordered list. Must never be None, can be [_('Unknown')]
'author_sort_map', # Map of sort strings for each author
# Pseudo field that can be set, but if not set is auto generated
# from authors and languages
'author_sort',
'book_producer',
'timestamp', # Dates and times must be timezone aware
'pubdate',
'last_modified',
'rights',
# So far only known publication type is periodical:calibre
# If None, means book
'publication_type',
'uuid', # A UUID usually of type 4
'languages', # ordered list of languages in this publication
'publisher', # Simple string, no special semantics
# Absolute path to image file encoded in filesystem_encoding
'cover',
# Of the form (format, data) where format is, e.g. 'jpeg', 'png', 'gif'...
'cover_data',
# Either thumbnail data, or an object with the attribute
# image_path which is the path to an image file, encoded
# in filesystem_encoding
'thumbnail',
))

BOOK_STRUCTURE_FIELDS = frozenset((
# These are used by code, Null values are None.
'toc', 'spine', 'guide', 'manifest',
))

USER_METADATA_FIELDS = frozenset((
# A dict of dicts similar to field_metadata. Each field description dict
# also contains a value field with the key #value#.
'user_metadata',
))

DEVICE_METADATA_FIELDS = frozenset((
'device_collections', # Ordered list of strings
'lpath', # Unicode, / separated
'size', # In bytes
'mime', # Mimetype of the book file being represented
))

CALIBRE_METADATA_FIELDS = frozenset((
'application_id', # An application id, currently set to the db_id.
'db_id', # the calibre primary key of the item.
'formats', # list of formats (extensions) for this book
# a dict of user category names, where the value is a list of item names
# from the book that are in that category
'user_categories',
# a dict of items to associated hyperlink
'link_maps',
# Calculated page count, null values are None or 0. -1 is no countable
# formats. -2 is error processing formats, -3 is DRMed.
'pages',
))

ALL_METADATA_FIELDS = SOCIAL_METADATA_FIELDS.union(
PUBLICATION_METADATA_FIELDS).union(
BOOK_STRUCTURE_FIELDS).union(
USER_METADATA_FIELDS).union(
DEVICE_METADATA_FIELDS).union(
CALIBRE_METADATA_FIELDS)

# All fields except custom fields
STANDARD_METADATA_FIELDS = SOCIAL_METADATA_FIELDS.union(
PUBLICATION_METADATA_FIELDS).union(
BOOK_STRUCTURE_FIELDS).union(
DEVICE_METADATA_FIELDS).union(
CALIBRE_METADATA_FIELDS)

# Metadata fields that smart update must do special processing to copy.
SC_FIELDS_NOT_COPIED = frozenset(('title', 'title_sort', 'authors',
'author_sort', 'author_sort_map',
'cover_data', 'tags', 'languages',
'identifiers'))

# Metadata fields that smart update should copy only if the source is not None
SC_FIELDS_COPY_NOT_NULL = frozenset(('device_collections', 'lpath', 'size', 'comments', 'thumbnail'))

# Metadata fields that smart update should copy without special handling
SC_COPYABLE_FIELDS = SOCIAL_METADATA_FIELDS.union(
PUBLICATION_METADATA_FIELDS).union(
BOOK_STRUCTURE_FIELDS).union(
DEVICE_METADATA_FIELDS).union(
CALIBRE_METADATA_FIELDS) - \ SC_FIELDS_NOT_COPIED.union(
SC_FIELDS_COPY_NOT_NULL)

SERIALIZABLE_FIELDS = SOCIAL_METADATA_FIELDS.union(
USER_METADATA_FIELDS).union(
PUBLICATION_METADATA_FIELDS).union(
CALIBRE_METADATA_FIELDS).union(
DEVICE_METADATA_FIELDS) - \ frozenset(('device_collections', 'formats',
'cover_data'))
# these are rebuilt when needed

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 튜토리얼 »
- calibre에서 정규 표현식 사용하기

calibre에서 정규 표현식 사용하기

정규 표현식은 전자책 콘텐츠 및 메타데이터의 정교한 조작을 수행하기 위해 calibre의 여러 곳에서 사용되는 기능입니다. 이 튜토리얼은 calibre에서 정규 표현식을 사용하는 방법을 쉽게 소개합니다.

목차

- 먼저, 경고와 용기의 말씀

- calibre 어디에서 정규 표현식을 사용할 수 있나요?

- 정규 표현식이 도대체 무엇인가요?

- 설명해 주시겠어요?

- 그리 나쁘지 않네요. 다음은요?

- 오, 깔끔하네요! 이해가 되기 시작합니다!

- 이 특수 문자들이 아주 깔끔하지만, 점이나 물음표와 일치시키고 싶으면 어떻게 하나요?

- 그럼 가장 유용한 집합은 무엇인가요?

- 하지만 일치시키고 싶은 몇 가지 변형된 문자열이 있으면 복잡해지나요?

- 놓친 것이 있는데…

- 처음에 정규 표현식의 대소문자 구분을 없애는 방법이 있다고 하셨죠?

- 이제 정규 표현식을 이해하기 시작한 것 같은데… calibre에서 어떻게 사용하나요?

- 변환

- 책 추가

- 메타데이터 일괄 편집

- 빠른 참조

- 크레딧

먼저, 경고와 용기의 말씀

이것은 어쩔 수 없이 약간 기술적일 것입니다. 어쨌든 정규 표현식은 기술적인 작업을 수행하는 기술적 도구이니까요. 복잡하거나 난해해 보일 수 있는 몇 가지 전문 용어와 개념을 사용해야 합니다. 가능한 한 명확하게 설명하려고 노력하겠지만, 전혀 사용하지 않을 수는 없습니다. 그렇다고 해도 새로운 것은 모두 설명하려고 했으므로 전문 용어에 좌절하지 마세요. 정규 표현식 자체는 신비롭고 마법 같은 것(또는 좀 더 평범하게 말하면 무작위의 난해한 문자와 기호의 나열)처럼 보일 수 있지만, 그렇게 복잡하지 않다고 약속합니다. 정규 표현식을 정말 잘 이해하는 사람들도 더 복잡한 것을 읽는 데 어려움을 겪지만, 작성하는 것은 그렇게 어렵지 않습니다. 표현식을 단계별로 구성하면 됩니다. 그럼 한 걸음 내딛고 토끼 굴 속으로 따라오세요.

calibre 어디에서 정규 표현식을 사용할 수 있나요?

calibre에서 정규 표현식을 사용하는 곳이 몇 군데 있습니다. 변환 옵션의 찾아 바꾸기, 가져오기 설정의 파일명에서 메타데이터 감지, 그리고 여러 책의 메타데이터를 일괄 편집할 때의 찾아 바꾸기가 있습니다. calibre 책 편집기에서도 찾아 바꾸기 기능에서 정규 표현식을 사용할 수 있습니다. 마지막으로 calibre 책 목록을 검색하거나 calibre 전자책 뷰어 안에서 검색할 때 정규 표현식을 사용할 수 있습니다.

정규 표현식이 도대체 무엇인가요?

정규 표현식은 문자열의 집합을 설명하는 방법입니다. 하나의 정규 표현식으로 여러 가지 다른 문자열과 *일치*시킬 수 있습니다. 이것이 정규 표현식이 강력한 이유입니다. 잠재적으로 많은 수의 변형을 간결하게 설명할 수 있는 방법이기 때문입니다.

참고

여기서 문자열은 프로그래밍 언어에서 사용되는 의미로 사용합니다. 즉, 하나 이상의 문자로 이루어진 문자열이며, 문자에는 실제 문자, 숫자, 구두점, 그리고 공백 문자(줄바꿈, 탭 등)가 포함됩니다. 일반적으로 대문자와 소문자는 같지 않은 것으로 간주됩니다. 따라서 “a”는 “A”와 다른 문자입니다. calibre에서 정규 표현식은 검색 표시줄에서는 대소문자를 구분하지 않지만, 변환 옵션에서는 대소문자를 구분합니다. 모든 정규 표현식의 대소문자 구분을 없애는 방법이 있지만, 나중에 설명하겠습니다. 정규 표현식이 일치하는 문자열의 변형을 허용하므로 하나의 표현식이 여러 문자열과 일치할 수 있어 복잡해집니다. 이것이 사람들이 정규 표현식을 사용하는 이유입니다. 이에 대해서는 조금 더 설명하겠습니다.

설명해 주시겠어요?

글쎄요, 그래서 우리가 여기 있는 거죠. 먼저, 정규 표현식에서 가장 중요한 개념입니다: 문자열 자체가 자기 자신과 일치하는 정규 표현식입니다. 즉, 정규 표현식을 사용하여 문자열 "Hello, World!"``과 일치시키고 싶다면 사용할 정규 표현식은 ``Hello, World!``입니다. 네, 정말 그렇게 간단합니다. 하지만 이것은 정확한 문자열 ``"Hello, World!"``와 *만* 일치하며, 예를 들어 ``"Hello, wOrld!"``나 ``"hello, world!" 또는 그 밖의 변형과는 일치하지 않는다는 점을 알 수 있습니다.

그리 나쁘지 않네요. 다음은요?

다음은 정말 좋은 것들의 시작입니다. 정규 표현식이 여러 문자열과 일치할 수 있다고 했던 것을 기억하시죠? 여기서부터 조금 더 복잡해집니다. 좀 더 실용적인 연습으로, 변환하려는 전자책에 “Page 5 of 423”과 같이 페이지를 세는 성가신 꼬리말이 있다고 가정합니다. 당연히 페이지 번호는 1부터 423까지 올라가므로 423개의 다른 문자열과 일치시켜야 할 것 같죠? 아닙니다. 정규 표현식은 일치시킬 문자의 집합을 정의할 수 있게 해줍니다. 집합을 정의하려면 집합에 포함하려는 모든 문자를 대괄호 안에 넣습니다. 예를 들어, 집합 [abc]``는 문자 "a", "b" 또는 "c" 중 하나와 일치합니다. *집합은 항상 집합 안의 문자 중 하나와만 일치합니다*. 문자 범위를 "이해"합니다. 즉, 모든 소문자와 일치시키려면 집합 ``[a-z]``를 사용하고, 대소문자 모두와 일치시키려면 ``[a-zA-Z]``를 사용하는 식입니다. 이해가 되시나요? 따라서 ``Page [0-9] of 423 표현식을 사용하면 처음 9페이지와 일치시킬 수 있으므로 필요한 표현식을 세 개로 줄일 수 있습니다. 두 번째 표현식 ``Page [0-9][0-9] of 423``은 모든 두 자리 페이지 번호와 일치하며, 세 번째 표현식이 어떻게 생겼을지 짐작이 되실 겁니다. 네, 적어보세요.

오, 깔끔하네요! 이해가 되기 시작합니다!

그렇게 말해 주시길 바랐습니다. 하지만 마음의 준비를 하세요, 이제 더 좋아집니다! 방금 집합을 사용하면 여러 문자 중 하나와 일치시킬 수 있다는 것을 보았습니다. 하지만 문자나 집합을 반복할 수도 있어서, 위의 페이지 번호 예시를 처리하는 데 필요한 표현식 수를 하나로 줄일 수 있습니다. 네, 하나요! 흥분되시나요? 되어야 합니다! 이렇게 작동합니다. 일부 특수 문자인 “+”, “?”, “*”는 앞에 오는 단일 요소를 반복합니다. (요소는 단일 문자, 문자 집합, 이스케이프 시퀀스 또는 그룹(나중에 배울 것입니다)을 의미합니다. 즉, 정규 표현식의 단일 엔티티). 이 문자를 와일드카드 또는 한정자라고 합니다. 더 정확하게 말하면, “?”는 앞 요소의 0개 또는 1개*와 일치하고, “”는 앞 요소의 *0개 이상*과 일치하며, “+”는 앞 요소의 *1개 이상*과 일치합니다. 몇 가지 예시: a? 표현식은 “”(빈 문자열, 이 경우 엄밀히 말하면 유용하지 않음) 또는 “a”와 일치하고, a* 표현식은 “”, “a”, “aa” 또는 연속된 임의의 수의 a와 일치하며, 마지막으로 a+ 표현식은 “a”, “aa” 또는 연속된 임의의 수의 a와 일치합니다(참고: 빈 문자열과는 일치하지 않습니다!). 집합도 마찬가지입니다. [0-9]+ 표현식은 *존재하는 모든 정수*와 일치합니다! 무슨 생각을 하고 계신지 알겠고, 맞습니다. 위의 페이지 번호 일치 사례에서 사용하면 모든 페이지 번호와 일치하는 단 하나의 표현식이 되지 않을까요? 네, Page [0-9]+ of 423 표현식은 그 책의 모든 페이지 번호와 일치합니다!

참고

이 한정자들에 대한 참고 사항: 일반적으로 가능한 한 많은 텍스트와 일치하려고 시도하므로 사용할 때 주의하세요. 이를 “탐욕스러운 동작”이라고 합니다. 왜 그런지 이해가 되시죠? 예를 들어 태그와 일치시키려고 할 때 문제가 됩니다. 예를 들어, 문자열 "<p class="calibre2">Title here</p>"``를 생각해 보세요. 여는 태그(첫 번째 꺾쇠괄호 쌍 사이의 부분, 태그에 대해서는 나중에 자세히 설명)와 일치시키고 싶다고 가정합니다. ``<p.*> 표현식이 해당 태그와 일치할 것이라고 생각할 수 있지만, 실제로는 전체 문자열과 일치합니다! (문자 “.”는 또 다른 특수 문자입니다. 줄바꿈을 제외한 모든 것과 일치하므로, 기본적으로 .* 표현식은 생각할 수 있는 모든 단일 줄과 일치합니다). 대신 <p.*?>``를 사용해 보세요. 이는 한정자 ``"*"``를 비탐욕적으로 만듭니다. 해당 표현식은 의도한 대로 첫 번째 여는 태그와만 일치합니다. 사실 이를 달성하는 또 다른 방법이 있습니다. ``<p[^>]*> 표현식은 동일한 여는 태그와 일치합니다. 다음 섹션 후에 이유를 알게 될 것입니다. 정규 표현식을 작성하는 방법이 종종 하나 이상이라는 점만 알아두세요.

이 특수 문자들이 아주 깔끔하지만, 점이나 물음표와 일치시키고 싶으면 어떻게 하나요?

물론 그렇게 할 수 있습니다. 특수 문자 앞에 백슬래시를 넣기만 하면 특별한 의미 없이 문자 그대로의 문자로 해석됩니다. 백슬래시 뒤에 단일 문자가 오는 이 쌍을 이스케이프 시퀀스라고 하며, 특수 문자 앞에 백슬래시를 넣는 행위를 해당 문자 이스케이프라고 합니다. 이스케이프 시퀀스는 단일 요소로 해석됩니다. 특수 문자 이스케이프 외에 더 많은 작업을 수행하는 이스케이프 시퀀스도 있습니다. 예를 들어 ``”t”``는 탭을 의미합니다. 일부 이스케이프 시퀀스는 나중에 다루겠습니다. 그리고 참고로, 이 특수 문자들에 관하여: 이 소개에서 논의하는 모든 문자를 특수한 기능을 가진 것으로 간주하여 문자 그대로의 문자를 원한다면 이스케이프해야 합니다.

그럼 가장 유용한 집합은 무엇인가요?

물론 물어보실 줄 알았습니다. 유용한 집합으로는 단일 숫자와 일치하는 [0-9], 단일 소문자와 일치하는 [a-z], 단일 대문자와 일치하는 [A-Z], 단일 문자와 일치하는 [a-zA-Z], 단일 문자 또는 숫자와 일치하는 ``[a-zA-Z0-9]``가 있습니다. 이스케이프 시퀀스를 단축형으로 사용할 수도 있습니다:

\d

``[0-9]``와 동일합니다

\w

``[a-zA-Z0-9_]``와 동일합니다

\s

모든 공백 문자와 동일합니다

참고

“공백 문자”는 인쇄되지 않는 모든 것을 가리키는 용어입니다. 이러한 문자에는 스페이스, 탭, 줄 바꿈, 폼 피드, 캐리지 리턴, 줄바꿈 없는 공백 등이 포함됩니다.

참고

대문자 및 소문자 집합은 검색의 대소문자 구분을 하지 않는 설정이 활성화된 경우 대문자와 소문자 모두와 일치할 수 있습니다. 이러한 설정은 예를 들어 calibre 자체의 환경설정->검색과 calibre 전자책 뷰어`의 검색 패널, 그리고 calibre :guilabel:`책 편집 도구에서 찾을 수 있습니다.

집합에 대한 마지막 참고 사항으로, 집합에 있는 문자 를 제외한 모든 문자로 집합을 정의할 수도 있습니다. "^" 문자를 집합의 가장 첫 번째 문자*로 포함하여 그렇게 합니다. 따라서 ``[^a]``는 “a”를 제외한 모든 문자와 일치합니다. 이를 집합의 여집합이라고 합니다. 이전에 본 이스케이프 시퀀스 단축형도 여집합을 만들 수 있습니다. ``”D”``는 숫자가 아닌 모든 문자를 의미하므로 ``[^0-9]``와 동일합니다. 짐작하셨겠지만, 다른 단축형도 해당 소문자 대신 대문자를 사용하여 여집합을 만들 수 있습니다. 따라서 이전 섹션의 ``<p[^>]>`` 예시로 돌아가면, 사용하고 있는 문자 집합이 닫는 꺾쇠괄호를 제외한 모든 문자와 일치하려고 한다는 것을 알 수 있습니다.

하지만 일치시키고 싶은 몇 가지 변형된 문자열이 있으면 복잡해지나요?

걱정하지 마세요, 여전히 쉽고 좋습니다. 이 예시를 생각해 보세요. 변환하려는 책에 홀수 페이지마다 “Title”이, 짝수 페이지마다 “Author”가 쓰여 있습니다. 인쇄본에서는 멋져 보이죠? 하지만 전자책에서는 성가십니다. 일반 괄호로 전체 표현식을 그룹화할 수 있으며, 문자 "|"``를 사용하면 오른쪽 표현식 *또는* 왼쪽 표현식과 일치시킬 수 있습니다. 이것들을 결합하면 됩니다. 너무 빠른가요? 먼저, 홀수 및 짝수 페이지에 대한 표현식을 그룹화하여 ``(Title)(Author)``라는 두 개의 필요한 표현식을 얻습니다. 이제 세로 막대(“|”는 세로 막대 문자라고 합니다)를 사용하여 더 간단하게 만듭니다. ``(Title|Author) 표현식을 사용하면 “Title”(홀수 페이지에서) 또는 “Author”(짝수 페이지에서)와 일치합니다. 쉽지 않나요?

물론 그룹화 괄호 없이 세로 막대를 사용할 수도 있습니다. 한정자가 앞에 오는 요소를 반복한다고 했던 것을 기억하시죠? 세로 막대는 약간 다르게 작동합니다. “Title|Author” 표현식은 위의 그룹화 예시처럼 문자열 “Title” 또는 문자열 “Author”와도 일치합니다. 세로 막대는 앞뒤의 전체 표현식 사이에서 선택합니다. 따라서 “Calibre”와 “calibre” 문자열과 일치시키고 싶은데 대문자 “C”와 소문자 “c” 사이에서만 선택하고 싶다면 (c|C)alibre 표현식을 사용해야 합니다. 여기서 그룹화가 “c”만 선택되도록 보장합니다. ``c|Calibre``를 사용하면 문자열 “c” 또는 문자열 “Calibre”와 일치하게 되어 우리가 원하는 것이 아닙니다. 요약하면: 의심스러우면 세로 막대와 함께 그룹화를 사용하세요.

놓친 것이 있는데…

… 잠깐만요, 그룹으로 할 수 있는 마지막으로 정말 깔끔한 것이 있습니다. 이전에 일치시킨 그룹이 있으면 나중에 표현식에서 해당 그룹에 대한 참조를 사용할 수 있습니다. 그룹은 1부터 번호가 매겨지며, 참조하려는 그룹의 번호를 이스케이프하여 참조합니다. 따라서 다섯 번째 그룹은 ``5``로 참조됩니다. 문자열 “Test Test”에서 ``([^ ]+) 1``을 검색하면 전체 문자열과 일치합니다!

처음에 정규 표현식의 대소문자 구분을 없애는 방법이 있다고 하셨죠?

네, 맞습니다. 주의 깊게 들어주시고 상기시켜 주셔서 감사합니다. 플래그라는 것을 사용하여 calibre에 특정 처리 방법을 지시할 수 있습니다. 표현식에 플래그를 포함하려면 ``(?플래그는 여기에)``라는 특수 구문을 사용합니다. 여기서 “플래그는 여기에”를 원하는 특정 플래그로 바꾸면 됩니다. 대소문자를 무시하려면 플래그가 ``i``이므로 표현식에 ``(?i)``를 포함합니다. 따라서 ``(?i)test``는 “Test”, “tEst”, “TEst” 그리고 생각할 수 있는 모든 대소문자 변형과 일치합니다.

또 다른 유용한 플래그는 점이 줄바꿈을 포함한 모든 문자와 일치하도록 하는 s 플래그입니다. 표현식에서 여러 플래그를 사용하려면 동일한 구문에 넣으면 됩니다. ``(?is)``는 대소문자를 무시하고 점이 모든 것과 일치하도록 합니다. 어떤 플래그를 먼저 표시하는지는 중요하지 않으며, ``(?si)``는 위와 동일합니다.

이제 정규 표현식을 이해하기 시작한 것 같은데… calibre에서 어떻게 사용하나요?

변환

변환 설정부터 시작하겠습니다. 정말 깔끔합니다. 찾아 바꾸기 부분에서 변환 중에 대체될 문자열을 설명하는 regexp(정규 표현식의 약어)를 입력할 수 있습니다. 깔끔한 부분은 마법사입니다. 마법사 지팡이를 클릭하면 변환 과정에서 calibre가 “보는” 것의 미리보기를 얻을 수 있습니다. 제거하려는 문자열까지 스크롤하여 선택하고 복사한 다음 창 상단의 regexp 필드에 붙여넣으세요. 페이지 번호 등 가변적인 부분이 있으면 집합과 한정자를 사용하여 커버하고, 특수 문자가 있으면 이스케이프하는 것을 잊지 마세요. 테스트 버튼을 누르면 calibre가 regexp를 사용할 경우 대체할 부분을 강조 표시합니다. 만족하면 확인을 누르고 변환하세요. 변환 소스에 이 예시와 같은 태그가 있으면 주의하세요:

Maybe, but the cops feel like you do, Anita. What's one more dead vampire?
New laws don't change that. </p>
<p class="calibre4"> <b class="calibre2">Generated by ABC Amber LIT Conv
<a href="http://www.processtext.com/abclit.html" class="calibre3">erter,
http://www.processtext.com/abclit.html</a></b></p>
<p class="calibre4"> It had only been two years since Addison v. Clark.
The court case gave us a revised version of what life was

(이 스레드 <https://www.mobileread.com/forums/showthread.php?t=75594>`_에서 당당히 가져옴). 일부 태그도 제거해야 합니다. 이 예시에서는 ``<b class=”calibre2”>` 태그로 시작하는 것을 권장합니다. 이제 해당 닫는 태그(여는 태그는 <tag>, 닫는 태그는 </tag>)로 끝나야 합니다. 이 경우 단순히 다음 ``</b>``입니다. (이 점이 명확하지 않으면 좋은 HTML 매뉴얼을 참조하거나 포럼에서 질문하세요). 여는 태그는 ``<b.*?>``로, 닫는 태그는 ``</b>``로 설명할 수 있으므로 ``<b.*?>.*?</b>``를 사용하여 해당 태그 사이의 모든 것을 제거할 수 있습니다. 그러나 이 표현식을 사용하면 <b> 태그(참고로 포함된 텍스트를 굵게 표시합니다)로 둘러싸인 모든 것이 제거되므로 이 방식으로 책의 일부를 제거할 가능성이 높습니다. 대신 포함된 문자열의 시작 부분도 포함하여 정규 표현식을 ``<b.*?>s*Generateds+bys+ABCs+Ambers+LIT.*?</b>``로 만드세요. 여기서 ``s``와 한정자는 문자열에 나타나는 공백을 명시적으로 사용하는 대신 발생할 수 있는 문자열의 변형을 포착하기 위해 포함됩니다. 새 표현식을 테스트할 때 calibre가 제거할 내용을 확인하여 유지하려는 부분을 제거하지 않도록 하세요. 한 번의 일치만 확인하면 텍스트의 다른 곳에서 불일치를 놓칠 수 있습니다. 또한 실수로 원하는 것보다 많거나 적은 태그를 제거한 경우 calibre가 제거 후 손상된 코드를 복구하려고 시도한다는 점도 알아두세요.

책 추가

정규 표현식을 사용할 수 있는 또 다른 방법은 파일명에서 메타데이터를 추출하는 것입니다. 이 기능은 설정의 “책 추가” 부분에서 찾을 수 있습니다. 여기에 특별한 기능이 있습니다. 메타데이터 필드에 필드 이름을 사용할 수 있습니다. 예를 들어 ``(?P<title>)``은 calibre가 문자열의 이 부분을 책 제목으로 사용한다는 것을 나타냅니다. 허용된 필드 이름은 창에 나열되어 있으며, 또 다른 좋은 테스트 필드와 함께 제공됩니다. 예시: ``Classical Texts: The Divine Comedy by Dante Alighieri.mobi``와 같이 이름이 지정된 파일 묶음을 가져오려고 합니다(분명히 모두 고전 이탈리아 시를 좋아하므로 이미 라이브러리에 있습니다). 또는 ``Science Fiction epics: The Foundation Trilogy by Isaac Asimov.epub``입니다. 이것은 분명히 calibre가 의미 있는 데이터를 추출할 수 없는 명명 체계입니다. 메타데이터 추출을 위한 표준 표현식은 ``(?P<title>.+) - (?P<author>[^_]+)``입니다. 여기서 작동하는 정규 표현식은 ``[a-zA-Z]+: (?P<title>.+) by (?P<author>.+)``입니다. 메타데이터 필드의 그룹 안에서는 필드가 실제로 일치하는 것을 설명하는 표현식을 사용해야 합니다. 또한 calibre가 제공하는 테스트 필드를 사용할 때 테스트 파일명에 파일 확장자를 추가해야 합니다. 그렇지 않으면 작동하는 표현식을 사용하더라도 일치하는 항목을 전혀 얻을 수 없습니다.

메타데이터 일괄 편집

마지막 부분은 메타데이터 필드의 정규 표현식 찾아 바꾸기`입니다. 라이브러리에서 여러 책을 선택하고 일괄 메타데이터 편집을 사용하여 접근할 수 있습니다. 이 마지막 기능을 사용할 때는 매우 주의하세요. 라이브러리에 **매우 나쁜 일**을 할 수 있습니다! 테스트 필드를 사용하여 표현식이 원하는 대로 작동하는지 다시 확인하고, 정말로 변경하려는 책만 표시하세요! 정규 표현식 검색 모드에서는 한 필드에서 검색하고, 텍스트를 다른 것으로 대체하고, 결과를 다른 필드에 쓸 수도 있습니다. 실용적인 예시: 라이브러리에 Frank Herbert의 Dune 시리즈 책이 있고 ``Dune 1 - Dune`, Dune 2 - Dune Messiah 등의 형식으로 이름이 지정되어 있다고 가정합니다. 이제 ``Dune``을 시리즈 필드에 넣고 싶습니다. 제목 필드에서 ``(.*?) d+ - .*``을 검색하고 시리즈 필드에서 ``1``로 대체하면 됩니다. 제가 한 것을 보셨나요? 그것은 시리즈 필드로 대체하는 첫 번째 그룹에 대한 참조입니다. 이제 시리즈가 모두 설정되었으므로, 제목 필드에서 ``.*? -``을 다시 검색하고 제목 필드에서 ``””``(빈 문자열)로 대체하면 메타데이터가 깔끔하게 정리됩니다. 굉장하지 않나요? 참고로, 전체 필드를 대체하는 대신 필드에 추가하거나 앞에 붙일 수도 있습니다. 따라서 원한다면 책 제목 앞에 시리즈 정보를 붙일 수도 있습니다. 이제 의심할 여지 없이 눈치채셨겠지만, :guilabel:`대소문자 구분`이라는 확인란이 있으므로 여기서 동작을 선택하기 위해 플래그를 사용할 필요가 없습니다.

정규 표현식에 대한 아주 짧은 소개가 이것으로 끝납니다. 최소한 시작하고 스스로 계속 배울 수 있을 만큼 충분히 보여드렸기를 바랍니다. 좋은 시작점은 `Python 정규 표현식 문서 <https://docs.python.org/library/re.html>`_입니다.

마지막 경고의 말씀: 정규 표현식은 강력하지만 정말로 잘못되기도 쉽습니다. calibre는 표현식이 예상대로 작동하는지 확인할 수 있는 정말 훌륭한 테스트 가능성을 제공합니다. 사용하세요. 자살골을 넣지 않도록 노력하세요. (이 표현 정말 좋아합니다…). 하지만 경고에도 불구하고 발(또는 다른 신체 부위)을 다치게 되면, 그것에서 배우도록 하세요.

빠른 참조

- 정규 표현식 구문 빠른 참조

- 문자 클래스
- 약어 문자 클래스
- 한정자
- 탐욕
- 대안
- 제외
- 앵커
- 그룹
- 룩어라운드
- 재귀
- 특수 문자
- 메타 문자
- 모드

크레딧

팁, 수정 등에 도움을 주셔서 감사합니다:

- ldolse

- kovidgoyal

- chaley

- dwanthny

- kacir

- Starson17

- Orpheu

정규 표현식에 대한 자세한 내용은 `Python 사용자 매뉴얼 <https://docs.python.org/library/re.html>`_을 참조하세요. calibre에서 실제로 사용하는 정규 표현식 라이브러리는 Python 표준 라이브러리보다 여러 유용한 개선 사항을 지원하는 `regex <https://bitbucket.org/mrabarnett/mrab-regex/src/hg/>`_입니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 튜토리얼 »
- calibre에서 정규 표현식 사용하기 »
- 정규 표현식 구문 빠른 참조

정규 표현식 구문 빠른 참조

이 체크리스트는 calibre의 대부분에서 사용 가능한 정규 표현식 엔진에서 가장 많이 사용되거나 기억하기 어려운 부분을 요약합니다.

목차

- 문자 클래스

- 약어 문자 클래스

- 한정자

- 탐욕

- 대안

- 제외

- 앵커

- 그룹

- 룩어라운드

- 재귀

- 특수 문자

- 메타 문자

- 모드

문자 클래스

문자 클래스는 다양한 문자 그룹을 간결하게 표현하는 데 유용합니다.

예시:

표현

클래스

[a-z]

소문자. 악센트 기호와 합자가 포함되지 않음

[a-z0-9]

a부터 z까지의 소문자 또는 0부터 9까지의 숫자

[A-Za-z-]

대문자 또는 소문자, 또는 대시. 클래스에 대시를 포함하려면 문자 범위를 지정하는 하이픈과 혼동되지 않도록 처음이나 끝에 놓아야 합니다

[^0-9]

숫자를 제외한 모든 문자. 클래스 시작 부분에 놓인 캐럿(^)은 해당 클래스의 문자를 제외합니다 (보수 클래스)

[[a-z]--[aeiouy]]

소문자 자음. 클래스 안에 클래스를 포함할 수 있습니다. -- 문자는 뒤에 오는 것을 제외합니다

[\w--[\d_]]

모든 문자 (외국어 악센트 문자 포함). 약어 클래스는 클래스 안에서 사용할 수 있습니다

예시:

<[^<>]+> to select an HTML tag

약어 문자 클래스

표현

클래스

\d

숫자 (``[0-9]``와 동일)

\D

숫자가 아닌 모든 문자 (``[^0-9]``와 동일)

\w

악센트 기호와 합자가 포함된 영숫자 문자 ([a-zA-Z0-9])

\W

“단어가 아닌” 모든 문자

\s

공백, 줄바꿈 없는 공백, 탭, 캐리지 리턴

\S

“공백이 아닌” 모든 문자

.

줄바꿈을 제외한 모든 문자. 줄바꿈 문자를 포함하려면 “dot all” 체크박스를 사용하거나 (?s) 정규식 수정자를 사용하십시오.

한정자

한정자

한정자 앞의 표현식 출현 횟수

?

표현식의 0 또는 1회 출현. ``{0,1}``과 동일

+

표현식의 1회 이상 출현. ``{1,}``과 동일

*

표현식의 0, 1 또는 그 이상 출현. ``{0,}``과 동일

{n}

표현식의 정확히 n회 출현

{min,max}

최소값과 최대값(포함) 사이의 출현 횟수

{min,}

최소값(포함)과 무한대 사이의 출현 횟수

{,max}

0과 최대값(포함) 사이의 출현 횟수

탐욕

기본적으로 한정자와 함께 사용하면 정규 표현식 엔진은 탐욕(greedy) 모드로 작동하여 선택을 가능한 한 많이 확장합니다. 이는 처음에는 종종 당황스러운 결과를 초래합니다. 한정자 뒤에 ``?``를 붙이면 게으른(lazy) 모드가 됩니다. 같은 표현식에 두 개를 넣지 마세요. 결과를 예측할 수 없습니다.

한정자를 중첩하는 것은 주의하세요. 예를 들어 (a*)* 패턴은 처리 시간을 기하급수적으로 증가시킵니다.

대안

정규 표현식에서 | 문자는 논리적 ``OR``입니다. 앞의 표현식이나 뒤의 표현식 중 하나가 일치할 수 있음을 의미합니다.

제외

방법 1

제외할_패턴(*SKIP)(*FAIL)|선택할_패턴

예시:

"Blabla"(*SKIP)(*FAIL)|Blabla

Blabla 또는 “Blabla 또는 Blabla” 문자열에서는 Blabla를 선택하지만, “Blabla”에서는 선택하지 않습니다.

방법 2

제외할_패턴\K|(선택할_패턴)

"Blabla"\K|(Blabla)

Blabla 또는 “Blabla 또는 Blabla” 문자열에서는 Blabla를 선택하지만, “Blabla”에서는 선택하지 않습니다.

앵커

앵커는 문자가 아닌 문자열의 논리적 위치를 일치시키는 방법입니다. 텍스트 처리에 가장 유용한 앵커는 다음과 같습니다:

\b

단어 경계, 즉 공백에서 비공백 문자로의 전환을 나타냅니다. 예를 들어 ``bsurd``를 사용하면 ``the surd``는 일치하지만 ``absurd``는 일치하지 않습니다.

^

줄의 시작과 일치합니다 (기본값인 다중 줄 모드에서)

$

줄의 끝과 일치합니다 (기본값인 다중 줄 모드에서)

\K

선택의 시작 위치를 패턴 내의 해당 위치로 재설정합니다. 일부 정규 표현식 엔진(calibre는 해당하지 않음)은 특히 한정자와 함께 사용할 때 가변 길이의 룩비하인드를 허용하지 않습니다. 이러한 엔진에서 ``K``를 사용할 수 있다면 가변 길이의 긍정적 룩비하인드와 동등하게 작성하여 이 제한을 없앨 수도 있습니다.

그룹

(표현식)

캡처 그룹은 선택을 저장하며, 나중에 검색 또는 바꾸기 패턴에서 ``n``으로 참조할 수 있습니다. 여기서 ``n``은 캡처 그룹의 순서 번호입니다 (읽기 순서에서 1부터 시작)

(?:표현식)

선택을 캡처하지 않는 그룹

(?>표현식)

원자적 그룹: 표현식이 만족되는 즉시 정규 표현식 엔진이 통과하며, 나머지 패턴이 실패하면 해당 표현식으로 다른 조합을 시도하기 위해 백트래킹하지 않습니다. 원자적 그룹은 캡처하지 않습니다.

(?|표현식)

분기 재설정 그룹: 표현식에 포함된 대안의 분기가 동일한 그룹 번호를 공유합니다

(?<이름>표현식)

“name”이라는 이름의 그룹. 선택한 항목은 나중에 search 패턴에서 (?P=name) 으로, *replace*에서 ``g<name>``으로 다시 불러올 수 있습니다. 두 개의 서로 다른 그룹이 동일한 이름을 사용할 수 있습니다.

룩어라운드

룩어라운드

의미

?=

긍정적 룩어헤드 (선택 뒤에 놓음)

?!

부정적 룩어헤드 (선택 뒤에 놓음)

?<=

긍정적 룩비하인드 (선택 앞에 놓음)

?<!

부정적 룩비하인드 (선택 앞에 놓음)

룩어헤드와 룩비하인드는 문자를 소비하지 않으며, 길이가 0이고 캡처하지 않습니다.它们是 원자적 그룹입니다: 어서션이 만족되는 즉시 정규 표현식 엔진이 통과하며, 나머지 패턴이 실패하면 룩어라운드 내에서 다른 조합을 시도하기 위해 백트래킹하지 않습니다.

문자열에서 여러 일치 항목을 찾을 때, 각 일치 시도의 시작 위치에서 룩비하인드는 현재 위치 이전의 문자를 검사할 수 있습니다. 따라서 문자열 123에서 패턴 (?<=\d)\d (숫자 뒤에 오는 숫자)는 이론적으로 2와 3을 선택해야 합니다. 반면에 ``dKd``는 첫 번째 선택 후 시작 위치가 3 바로 앞이므로 2만 선택할 수 있으며, 두 번째 일치를 위한 충분한 숫자가 없습니다. 마찬가지로 ``d(d)``는 2만 캡처합니다. calibre의 정규 표현식 엔진에서는 긍정적 룩비하인드가 이론과 달리 동일한 방식으로 작동하여 2만 선택합니다.

그룹을 룩어라운드 안에 넣을 수 있지만, 캡처는 거의 유용하지 않습니다. 그럼에도 불구하고 유용한 경우, 룩비하인드에서 한정자를 사용할 때 매우 주의해야 합니다: 백트래킹의 부재와 관련된 탐욕은 예상치 못한 캡처를 초래할 수 있습니다. 이러한 이유로 긍정적 룩비하인드의 캡처 그룹에 한정자(또는 더 나쁜 경우 여러 개)가 있을 때는 긍정적 룩비하인드 대신 ``K``를 사용하세요.

부정적 룩어헤드 예시:

(?![^<>{}]*[>}])

패턴 끝에 놓으면 파일에 임베디드된 태그나 스타일 안에서 선택하는 것을 방지합니다.

가능하면 항상 룩어라운드를 “앵커링”하여 결과를 얻기 위해 필요한 단계 수를 줄이는 것이 좋습니다.

재귀

표현

의미

(?R)

전체 패턴의 재귀

(?1)

번호가 매겨진 캡처 그룹(여기서는 그룹 1)의 패턴만 재귀

재귀는 자기 자신을 호출하는 것입니다. 이는 중첩된 따옴표 문자열을 포함할 수 있는 따옴표 문자열 같은 균형 잡힌 쿼리에 유용합니다. 따라서 큰따옴표 사이의 문자열을 처리하는 동안 새로운 큰따옴표 문자열의 시작을 만나면, 어떻게 해야 하는지 알고 있으므로 자기 자신을 호출합니다. 그러면 다음과 같은 패턴을 갖게 됩니다:

start-pattern(?>atomic sub-pattern|(?R))*end-pattern

중첩된 문자열에서 멈추지 않고 큰따옴표 사이의 문자열을 선택하려면:

“((?>[^“”]+|(?R))*[^“”]+)”

이 템플릿은 <div> 태그처럼 중첩될 수 있는 태그 쌍을 수정하는 데도 사용할 수 있습니다.

특수 문자

표현

문자

\t

탭

\n

줄바꿈

\x20

(끊어질 수 있는) 공백

\xa0

줄바꿈 없는 공백

메타 문자

메타 문자는 정규 표현식 엔진에 특별한 의미를 갖는 문자입니다. 이 중 12개는 이스케이프 문자인 백슬래시(\)를 앞에 붙여야 특별한 의미를 잃고 일반 문자가 됩니다:

^ . [ ] $ ( ) * + ? | \

또 다른 7개의 메타 문자는 백슬래시를 앞에 붙일 필요가 없습니다 (다른 결과 없이 붙일 수도 있습니다):

{ } ! < > = :

특수 문자는 클래스 안(괄호 [] 사이)에서 사용하면 특수 문자 지위를 잃습니다. 닫는 괄호와 대시는 클래스 안에서 특수한 지위를 가집니다. 클래스 밖에서 대시는 단순 리터럴이고, 닫는 괄호는 메타 문자로 유지됩니다.

슬래시(/)와 우물 정자(또는 해시 문자)(#)는 메타 문자가 아니므로 이스케이프할 필요가 없습니다.

Python 엔진을 사용하는 regex101.com 같은 일부 도구에서는 큰따옴표가 구분자로서 특별한 지위를 가지며, 이스케이프하거나 옵션을 변경해야 합니다. calibre의 편집기에서는 그렇지 않습니다.

모드

(?s)

점(.)이 줄바꿈 문자도 일치하도록 합니다

(?m)

^``와 ``$ 앵커가 전체 문자열의 시작과 끝이 아닌 줄의 시작과 끝을 일치하도록 합니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 튜토리얼 »

- 직접 플러그인을 작성하여 calibre 기능 확장하기

직접 플러그인을 작성하여 calibre 기능 확장하기
calibre는 매우 모듈화된 설계를 가지고 있습니다. calibre의 거의 모든 기능이 플러그인 형태로 제공됩니다. 플러그인은 변환, 뉴스 다운로드(이것은 레시피라고 합니다), 사용자 인터페이스의 다양한 구성 요소, 다양한 장치 연결, calibre에 파일 추가 시 파일 처리 등에 사용됩니다. :guilabel:`환경설정->고급->플러그인`으로 이동하여 calibre의 모든 내장 플러그인의 전체 목록을 확인할 수 있습니다.

여기에서는 calibre에 새 기능을 추가하기 위해 직접 플러그인을 만드는 방법을 설명합니다.

목차

- calibre 플러그인의 구조

- 사용자 인터페이스 플러그인

- __init__.py

- ui.py

- main.py

- 플러그인 ZIP 파일에서 리소스 가져오기

- 플러그인의 사용자 구성 활성화

- 책 편집 플러그인

- main.py

- 플러그인에 번역 추가

- 플러그인 API

- 플러그인 디버깅

- 더 많은 플러그인 예시

- 다른 사람과 플러그인 공유

참고

이것은 calibre 릴리스 >= 0.8.60에만 적용됩니다

calibre 플러그인의 구조
calibre 플러그인은 매우 간단합니다. 일부 Python 코드와 플러그인에 필요한 이미지 파일과 같은 기타 리소스가 포함된 ZIP 파일일 뿐입니다. 바로 기본 예시를 살펴보겠습니다.

EPUB 및 MOBI 형식의 다양한 전자 문서를 자체 발행하는 데 사용하는 calibre 설치가 있다고 가정합니다. calibre에서 생성된 모든 파일의 발행자를 “Hello world”로 설정하고 싶다면 다음 방법을 따르세요. :file:`__init__.py`라는 파일을 만드세요(이것은 특별한 이름이며 플러그인의 메인 파일에 항상 사용해야 합니다). 그리고 다음 Python 코드를 입력하세요:

from calibre.customize import FileTypePlugin

class HelloWorld(FileTypePlugin):

name = 'Hello World Plugin' # Name of the plugin
description = 'Set the publisher to Hello World for all new conversions'
supported_platforms = ['windows', 'osx', 'linux'] # Platforms this plugin will run on
author = 'Acme Inc.' # The author of this plugin
version = (1, 0, 0) # The version number of this plugin
file_types = {'epub', 'mobi'} # The file types that this plugin will be applied to
on_postprocess = True # Run this plugin after conversion is complete
minimum_calibre_version = (0, 7, 53)

def run(self, path_to_ebook):
from calibre.ebooks.metadata.meta import get_metadata, set_metadata
with open(path_to_ebook, 'r+b') as file:
ext = os.path.splitext(path_to_ebook)[-1][1:].lower()
mi = get_metadata(file, ext)
mi.publisher = 'Hello World'
set_metadata(file, mi, ext)
return path_to_ebook

이것이 전부입니다. 이 코드를 플러그인으로 calibre에 추가하려면 :file:`__init__.py`를 만든 폴더에서 다음을 실행하세요:

calibre-customize -b .

참고
macOS에서는 명령줄 도구가 calibre 번들 안에 있습니다. 예를 들어, calibre를 /Applications`에 설치한 경우 명령줄 도구는 :file:/Applications/calibre.app/Contents/MacOS/`에 있습니다.

:download_file:`helloworld_plugin.zip`에서 Hello World 플러그인을 다운로드할 수 있습니다.

calibre를 사용하여 책을 변환할 때마다 플러그인의 run() 메서드가 호출되고 변환된 책의 발행자가 “Hello World”로 설정됩니다. 이것은 간단한 플러그인이며, 이제 실제로 사용자 인터페이스에 구성 요소를 추가하는 더 복잡한 예시로 넘어가겠습니다.

사용자 인터페이스 플러그인

이 플러그인은 여러 파일에 걸쳐 있습니다(코드를 깔끔하게 유지하기 위해). 플러그인 ZIP 파일에서 리소스(이미지 또는 데이터 파일)를 가져오는 방법, 사용자가 플러그인을 구성할 수 있게 하는 방법, calibre 사용자 인터페이스에 요소를 만드는 방법, calibre의 책 데이터베이스에 접근하고 쿼리하는 방법을 보여줍니다.

:download_file:`interface_demo_plugin.zip`에서 이 플러그인을 다운로드할 수 있습니다

먼저 이 ZIP 파일에는 아래 설명된 것처럼 더 많은 파일이 포함되어 있습니다. ``plugin-import-name-interface_demo.txt``에 특히 주의하세요.

plugin-import-name-interface_demo.txt

다중 파일 플러그인 기능을 활성화하는 데 사용되는 빈 텍스트 파일입니다. 이 파일은 하나 이상의 .py 파일을 사용하는 모든 플러그인에 있어야 합니다. 비어 있어야 하며 파일명은 plugin-import-name-**some_name**.txt 형식이어야 합니다. 이 파일이 있으면 다음과 같은 문을 사용하여 ZIP 파일 안에 있는 .py 파일에서 코드를 가져올 수 있습니다:

from calibre_plugins.some_name.some_module import some_object

접두사 ``calibre_plugins``는 항상 있어야 합니다. ``some_name``은 빈 텍스트 파일의 파일명에서 옵니다. ``some_module``은 ZIP 파일 안의 some_module.py 파일을 가리킵니다. 이 가져오기는 일반 Python 가져오기만큼 강력합니다. ZIP 파일 안에 .py 모듈의 패키지와 하위 패키지를 평소처럼(각 하위 폴더에 __init__.py를 정의하여) 만들 수 있으며, 모든 것이 “그냥 작동”해야 합니다.

``some_name``에 사용하는 이름은 모든 플러그인이 공유하는 전역 네임스페이스에 들어갑니다. 따라서 가능한 한 고유하게 만드세요. 유효한 Python 식별자(알파벳, 숫자, 밑줄만 가능)여야 한다는 점을 기억하세요.

__init__.py

이전과 같이, 플러그인 클래스를 정의하는 파일

main.py

이 파일에는 유용한 작업을 수행하는 실제 코드가 포함되어 있습니다

ui.py

이 파일은 플러그인의 인터페이스 부분을 정의합니다

images/icon.png

이 플러그인의 아이콘

about.txt

플러그인에 대한 정보가 포함된 텍스트 파일

번역

플러그인의 사용자 인터페이스를 다양한 언어로 번역한 .mo 파일이 포함된 폴더입니다. 자세한 내용은 아래를 참조하세요.

이제 코드를 살펴보겠습니다.

__init__.py

먼저, 플러그인 메타데이터를 정의하는 필수 ``__init__.py``입니다:

from calibre.customize import InterfaceActionBase

class InterfacePluginDemo(InterfaceActionBase):
'''
This class is a simple wrapper that provides information about the actual
plugin class. The actual interface plugin class is called InterfacePlugin
and is defined in the ui.py file, as specified in the actual_plugin field
below.

The reason for having two classes is that it allows the command line
calibre utilities to run without needing to load the GUI libraries.
'''
name = 'Interface Plugin Demo'
description = 'An advanced plugin demo'
supported_platforms = ['windows', 'osx', 'linux']
author = 'Kovid Goyal'
version = (1, 0, 0)
minimum_calibre_version = (0, 7, 53)

#: This field defines the GUI plugin class that contains all the code
#: that actually does something. Its format is module_path:class_name
#: The specified class must be defined in the specified module.
actual_plugin = 'calibre_plugins.interface_demo.ui:InterfacePlugin'

def is_customizable(self):
'''
This method must return True to enable customization via
Preferences->Plugins
'''
return True

def config_widget(self):
'''
Implement this method and :meth:`save_settings` in your plugin to
use a custom configuration dialog.

This method, if implemented, must return a QWidget. The widget can have
an optional method validate() that takes no arguments and is called
immediately after the user clicks OK. Changes are applied if and only
if the method returns True.

If for some reason you cannot perform the configuration at this time,
return a tuple of two strings (message, details), these will be
displayed as a warning dialog to the user and the process will be
aborted.

The base class implementation of this method raises NotImplementedError
so by default no user configuration is possible.
'''
# It is important to put this import statement here rather than at the
# top of the module as importing the config class will also cause the
# GUI libraries to be loaded, which we do not want when using calibre
# from the command line
from calibre_plugins.interface_demo.config import ConfigWidget
return ConfigWidget()

def save_settings(self, config_widget):
'''
Save the settings specified by the user with config_widget.

:param config_widget: The widget returned by :meth:`config_widget`.
'''
config_widget.save_settings()

# Apply the changes
ac = self.actual_plugin_
if ac is not None:
ac.apply_settings()

유일하게 주목할 만한 기능은 actual_plugin 필드입니다. calibre에는 명령줄과 GUI 인터페이스가 모두 있으므로, 이와 같은 GUI 플러그인은 __init__.py에서 GUI 라이브러리를 로드하면 안 됩니다. actual_plugin 필드는 실제 플러그인이 ZIP 아카이브 안의 다른 파일에 있으며 GUI 컨텍스트에서만 로드된다는 것을 calibre에 알려줌으로써 이를 대신 처리합니다.

이것이 작동하려면 위에서 설명한 것처럼 플러그인 ZIP 파일에 plugin-import-name-some_name.txt 파일이 있어야 합니다.

또한 플러그인의 사용자 구성을 활성화하는 몇 가지 방법이 있습니다. 이러한 내용은 아래에서 설명합니다.

ui.py

이제 실제 GUI 플러그인을 정의하는 ui.py를 살펴보겠습니다. 소스 코드에 주석이 많이 달려 있어 자체적으로 설명이 될 것입니다:

from calibre.gui2.actions import InterfaceAction
from calibre_plugins.interface_demo.main import DemoDialog

class InterfacePlugin(InterfaceAction):

name = 'Interface Plugin Demo'

# Declare the main action associated with this plugin
# The keyboard shortcut can be None if you don't want to use a keyboard
# shortcut. Remember that currently calibre has no central management for
# keyboard shortcuts, so try to use an unusual/unused shortcut.
action_spec = ('Interface Plugin Demo', None,
'Run the Interface Plugin Demo', 'Ctrl+Shift+F1')

def genesis(self):
# This method is called once per plugin, do initial setup here

# Set the icon for this interface action
# The get_icons function is a builtin function defined for all your
# plugin code. It loads icons from the plugin zip file. It returns
# QIcon objects, if you want the actual data, use the analogous
# get_resources builtin function.
#
# Note that if you are loading more than one icon, for performance, you
# should pass a list of names to get_icons. In this case, get_icons
# will return a dictionary mapping names to QIcons. Names that
# are not found in the zip file will result in null QIcons.
icon = get_icons('images/icon.png', 'Interface Demo Plugin')

# The qaction is automatically created from the action_spec defined
# above
self.qaction.setIcon(icon)
self.qaction.triggered.connect(self.show_dialog)

def show_dialog(self):
# The base plugin object defined in __init__.py
base_plugin_object = self.interface_action_base_plugin
# Show the config dialog
# The config dialog can also be shown from within
# Preferences->Plugins, which is why the do_user_config
# method is defined on the base plugin class
do_user_config = base_plugin_object.do_user_config

# self.gui is the main calibre GUI. It acts as the gateway to access
# all the elements of the calibre user interface, it should also be the
# parent of the dialog
d = DemoDialog(self.gui, self.qaction.icon(), do_user_config)
d.show()

def apply_settings(self):
from calibre_plugins.interface_demo.config import prefs
# In an actual non trivial plugin, you would probably need to
# do something based on the settings in prefs
prefs

main.py

인터페이스 플러그인 데모 대화상자를 구현하는 실제 로직입니다.

from calibre_plugins.interface_demo.config import prefs class DemoDialog(QDialog):

def __init__(self, gui, icon, do_user_config):
QDialog.__init__(self, gui)
self.gui = gui self.do_user_config = do_user_config

# The current database shown in the GUI
# db is an instance of the class LibraryDatabase from db/legacy.py
# This class has many, many methods that allow you to do a lot of
# things. For most purposes you should use db.new_api, which has
# a much nicer interface from db/cache.py self.db = gui.current_db self.l = QVBoxLayout()
self.setLayout(self.l)

self.label = QLabel(prefs['hello_world_msg'])
self.l.addWidget(self.label)

self.setWindowTitle('Interface Plugin Demo')
self.setWindowIcon(icon)

self.about_button = QPushButton('About', self)
self.about_button.clicked.connect(self.about)
self.l.addWidget(self.about_button)

self.marked_button = QPushButton(
'Show books with only one format in the calibre GUI', self)
self.marked_button.clicked.connect(self.marked)
self.l.addWidget(self.marked_button)

self.view_button = QPushButton(
'View the most recently added book', self)
self.view_button.clicked.connect(self.view)
self.l.addWidget(self.view_button)

self.update_metadata_button = QPushButton(
"Update metadata in a book's files", self)
self.update_metadata_button.clicked.connect(self.update_metadata)
self.l.addWidget(self.update_metadata_button)

self.conf_button = QPushButton(
'Configure this plugin', self)
self.conf_button.clicked.connect(self.config)
self.l.addWidget(self.conf_button)

self.resize(self.sizeHint())

def about(self):
# Get the about text from a file inside the plugin zip file
# The get_resources function is a builtin function defined for all your
# plugin code. It loads files from the plugin zip file. It returns
# the bytes from the specified file.
#
# Note that if you are loading more than one file, for performance, you
# should pass a list of names to get_resources. In this case,
# get_resources will return a dictionary mapping names to bytes. Names that
# are not found in the zip file will not be in the returned dictionary.
text = get_resources('about.txt')
QMessageBox.about(self, 'About the Interface Plugin Demo', text.decode('utf-8'))

def marked(self):
''' Show books with only one format '''
db = self.db.new_api matched_ids = {book_id for book_id in db.all_book_ids() if len(db.formats(book_id)) == 1}
# Mark the records with the matching ids
# new_api does not know anything about marked books, so we use the full
# db object self.db.set_marked_ids(matched_ids)

# Tell the GUI to search for all marked records self.gui.search.setEditText('marked:true')
self.gui.search.do_search()

def view(self):
''' View the most recently added book '''
most_recent = most_recent_id = None db = self.db.new_api for book_id, timestamp in db.all_field_for('timestamp', db.all_book_ids()).items():
if most_recent is None or timestamp > most_recent:
most_recent = timestamp most_recent_id = book_id if most_recent_id is not None:
# Get a reference to the View plugin view_plugin = self.gui.iactions['View']
# Ask the view plugin to launch the viewer for row_number view_plugin._view_calibre_books([most_recent_id])

def update_metadata(self):
'''
Set the metadata in the files in the selected book's record to match the current metadata in the database.
'''
from calibre.ebooks.metadata.meta import set_metadata from calibre.gui2 import error_dialog, info_dialog

# Get currently selected books rows = self.gui.library_view.selectionModel().selectedRows()
if not rows or len(rows) == 0:
return error_dialog(self.gui, 'Cannot update metadata',
'No books selected', show=True)
# Map the rows to book ids ids = list(map(self.gui.library_view.model().id, rows))
db = self.db.new_api for book_id in ids:
# Get the current metadata for this book from the db mi = db.get_metadata(book_id, get_cover=True, cover_as_data=True)
fmts = db.formats(book_id)
if not fmts:
continue for fmt in fmts:
fmt = fmt.lower()
# Get a python file object for the format. This will be either
# an in memory file or a temporary on disk file ffile = db.format(book_id, fmt, as_file=True)
ffile.seek(0)
# Set metadata in the format set_metadata(ffile, mi, fmt)
ffile.seek(0)
# Now replace the file in the calibre library with the updated
# file. We don't use add_format_with_hooks as the hooks were
# already run when the file was first added to calibre.
db.add_format(book_id, fmt, ffile, run_hooks=False)

info_dialog(self, 'Updated files', f'Updated the metadata in the files of {len(ids)} book(s)', show=True)

def config(self):
self.do_user_config(parent=self)
# Apply the changes self.label.setText(prefs['hello_world_msg'])

플러그인 ZIP 파일에서 리소스 가져오기
calibre의 플러그인 로딩 시스템은 플러그인 ZIP 파일에서 파일을 편리하게 가져올 수 있는 몇 가지 내장 함수를 정의합니다.

get_resources(name_or_list_of_names)

이 함수는 ZIP 파일 안의 파일 경로 목록과 함께 호출해야 합니다. 예를 들어 ZIP 파일의 images 폴더에 있는 icon.png 파일에 접근하려면 ``images/icon.png``를 사용하세요. Windows에서도 항상 정방향 슬래시를 경로 구분자로 사용하세요. 단일 이름을 전달하면 해당 파일의 원시 바이트를 반환하고, ZIP 파일에서 이름을 찾을 수 없으면 None을 반환합니다. 둘 이상의 이름을 전달하면 이름을 바이트에 매핑하는 딕셔너리를 반환합니다. 이름을 찾을 수 없으면 반환된 딕셔너리에 포함되지 않습니다.

get_icons(name_or_list_of_names, plugin_name=’’)

get_resources()의 래퍼로, get_resources가 반환한 원시 바이트에서 QIcon 객체를 생성합니다. ZIP 파일에서 이름을 찾을 수 없으면 해당 QIcon은 null이 됩니다. 아이콘 테마를 지원하려면 플러그인의 사용자 친화적인 이름을 ``plugin_name``으로 전달하세요. 사용자가 플러그인용 아이콘이 포함된 아이콘 테마를 사용 중이면 해당 아이콘이 우선적으로 로드됩니다.

플러그인의 사용자 구성 활성화

사용자가 플러그인을 구성할 수 있도록 하려면 기본 플러그인 클래스에 is_customizable, config_widget, save_settings 세 가지 메서드를 정의해야 합니다. 아래와 같습니다:

def is_customizable(self):
'''
This method must return True to enable customization via
Preferences->Plugins
'''
return True

def config_widget(self):
'''
Implement this method and :meth:`save_settings` in your plugin to
use a custom configuration dialog.

This method, if implemented, must return a QWidget. The widget can have
an optional method validate() that takes no arguments and is called
immediately after the user clicks OK. Changes are applied if and only
if the method returns True.

If for some reason you cannot perform the configuration at this time,
return a tuple of two strings (message, details), these will be
displayed as a warning dialog to the user and the process will be
aborted.

The base class implementation of this method raises NotImplementedError
so by default no user configuration is possible.
'''
# It is important to put this import statement here rather than at the
# top of the module as importing the config class will also cause the
# GUI libraries to be loaded, which we do not want when using calibre
# from the command line
from calibre_plugins.interface_demo.config import ConfigWidget
return ConfigWidget()

def save_settings(self, config_widget):
'''
Save the settings specified by the user with config_widget.

:param config_widget: The widget returned by :meth:`config_widget`.
'''
config_widget.save_settings()

# Apply the changes
ac = self.actual_plugin_
if ac is not None:
ac.apply_settings()

calibre에는 구성 데이터를 저장하는 다양한 방법이 있습니다(오랜 역사의 유산). 권장되는 방법은 구성 정보를 .json 파일에 저장하는 JSONConfig 클래스를 사용하는 것입니다.

데모 플러그인에서 구성 데이터를 관리하는 코드는 config.py에 있습니다:

from calibre.utils.config import JSONConfig

# This is where all preferences for this plugin will be stored
# Remember that this name (i.e. plugins/interface_demo) is also
# in a global namespace, so make it as unique as possible.
# You should always prefix your config file name with plugins/,
# so as to ensure you don't accidentally clobber a calibre config file
prefs = JSONConfig('plugins/interface_demo')

# Set defaults
prefs.defaults['hello_world_msg'] = 'Hello, World!'

class ConfigWidget(QWidget):

def __init__(self):
QWidget.__init__(self)
self.l = QHBoxLayout()
self.setLayout(self.l)

self.label = QLabel('Hello world &message:')
self.l.addWidget(self.label)

self.msg = QLineEdit(self)
self.msg.setText(prefs['hello_world_msg'])
self.l.addWidget(self.msg)
self.label.setBuddy(self.msg)

def save_settings(self):
prefs['hello_world_msg'] = self.msg.text()

이제 prefs 객체는 간단한 구문을 통해 플러그인 코드 전체에서 사용할 수 있습니다:

from calibre_plugins.interface_demo.config import prefs

main.py에서 prefs 객체가 사용되는 것을 확인할 수 있습니다:

def config(self):
self.do_user_config(parent=self)
# Apply the changes
self.label.setText(prefs['hello_world_msg'])

책 편집 플러그인

이제 방향을 바꾸어 calibre 책 편집기에 도구를 추가하는 플러그인을 만드는 방법을 살펴보겠습니다. 플러그인은 여기서 다운로드할 수 있습니다: editor_demo_plugin.zip [https://calibre-ebook.com/downloads/editor_demo_plugin.zip].

모든 플러그인과 마찬가지로 첫 번째 단계는 :ref:`위 <import_name_txt>`에서 설명한 대로 가져오기 이름 빈 txt 파일을 만드는 것입니다. 파일명은 ``plugin-import-name-editor_plugin_demo.txt``로 하겠습니다.

이제 플러그인에 대한 메타데이터(이름, 저자, 버전 등)가 포함된 필수 __init__.py 파일을 만듭니다.

class DemoPlugin(EditBookToolPlugin):

name = 'Edit Book plugin demo'
version = (1, 0, 0)
author = 'Kovid Goyal'
supported_platforms = ['windows', 'osx', 'linux']
description = 'A demonstration of the plugin interface for the ebook editor'
minimum_calibre_version = (1, 46, 0)

하나의 에디터 플러그인은 여러 도구를 제공할 수 있으며, 각 도구는 툴바의 버튼 하나와 에디터의 플러그인 메뉴 항목 하나에 해당합니다. 도구에 여러 관련 작업이 있는 경우 하위 메뉴를 포함할 수 있습니다.

모든 도구는 플러그인의 main.py 파일에 정의되어야 합니다. 모든 도구는 calibre.gui2.tweak_book.plugin.Tool 클래스를 상속하는 클래스입니다. 데모 플러그인의 ``main.py``를 살펴보겠습니다. 소스 코드에 주석이 많이 달려 있어 자체적으로 설명이 될 것입니다. 자세한 내용은 calibre.gui2.tweak_book.plugin.Tool 클래스의 API 문서를 참조하세요.

main.py

여기에서는 사용자가 제공한 숫자로 책의 모든 글꼴 크기를 곱하는 단일 도구의 정의를 볼 수 있습니다. 이 도구는 직접 플러그인을 개발하는 데 필요한 다양한 중요한 개념을 보여주므로 (주석이 많이 달린) 소스 코드를 주의 깊게 읽어야 합니다.

from css_parser.css import CSSRule from qt.core import QAction, QInputDialog from calibre import force_unicode from calibre.ebooks.oeb.polish.container import OEB_DOCS, OEB_STYLES, serialize from calibre.gui2 import error_dialog

# The base class that all tools must inherit from from calibre.gui2.tweak_book.plugin import Tool class DemoTool(Tool):

#: Set this to a unique name it will be used as a key name = 'demo-tool'

#: If True the user can choose to place this tool in the plugins toolbar allowed_in_toolbar = True

#: If True the user can choose to place this tool in the plugins menu allowed_in_menu = True def create_action(self, for_toolbar=True):
# Create an action, this will be added to the plugins toolbar and
# the plugins menu ac = QAction(get_icons('images/icon.png'), 'Magnify fonts', self.gui) # noqa: F821 if not for_toolbar:
# Register a keyboard shortcut for this toolbar action. We only
# register it for the action created for the menu, not the toolbar,
# to avoid a double trigger self.register_shortcut(ac, 'magnify-fonts-tool', default_keys=('Ctrl+Shift+Alt+D',))
ac.triggered.connect(self.ask_user)
return ac def ask_user(self):
# Ask the user for a factor by which to multiply all font sizes factor, ok = QInputDialog.getDouble( self.gui, 'Enter a magnification factor', 'Allow font sizes in the book will be multiplied by the specified factor', value=2, min=0.1, max=4
)
if ok:
# Ensure any in progress editing the user is doing is present in the container self.boss.commit_all_editors_to_container()
try:
self.magnify_fonts(factor)
except Exception:
# Something bad happened report the error to the user import traceback error_dialog(self.gui, _('Failed to magnify fonts'), _(
'Failed to magnify fonts, click "Show details" for more info'), det_msg=traceback.format_exc(), show=True)
# Revert to the saved restore point self.boss.revert_requested(self.boss.global_undo.previous_container)
else:
# Show the user what changes we have made, allowing her to
# revert them if necessary self.boss.show_current_diff()
# Update the editor UI to take into account all the changes we
# have made self.boss.apply_container_update_to_gui()

def magnify_fonts(self, factor):
# Magnify all font sizes defined in the book by the specified factor
# First we create a restore point so that the user can undo all changes
# we make.
self.boss.add_savepoint('Before: Magnify fonts')

container = self.current_container # The book being edited as a container object

# Iterate over all style declarations in the book, this means css
# stylesheets, <style> tags and style="" attributes for name, media_type in container.mime_map.items():
if media_type in OEB_STYLES:
# A stylesheet. Parsed stylesheets are css_parser CSSStylesheet
# objects.
self.magnify_stylesheet(container.parsed(name), factor)
container.dirty(name) # Tell the container that we have changed the stylesheet elif media_type in OEB_DOCS:
# A HTML file. Parsed HTML files are lxml elements for style_tag in container.parsed(name).xpath('//*[local-name="style"]'):
if style_tag.text and style_tag.get('type', None) in {None, 'text/css'}:
# We have an inline CSS <style> tag, parse it into a
# stylesheet object sheet = container.parse_css(style_tag.text)
self.magnify_stylesheet(sheet, factor)
style_tag.text = serialize(sheet, 'text/css', pretty_print=True)
container.dirty(name) # Tell the container that we have changed the stylesheet for elem in container.parsed(name).xpath('//*[@style]'):
# Process inline style attributes block = container.parse_css(elem.get('style'), is_declaration=True)
self.magnify_declaration(block, factor)
elem.set('style', force_unicode(block.getCssText(separator=' '), 'utf-8'))

def magnify_stylesheet(self, sheet, factor):
# Magnify all fonts in the specified stylesheet by the specified
# factor.
for rule in sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE):
self.magnify_declaration(rule.style, factor)

def magnify_declaration(self, style, factor):
# Magnify all fonts in the specified style declaration by the specified
# factor val = style.getPropertyValue('font-size')
if not val:
return
# see if the font-size contains a number num = re.search(r'[0-9.]+', val)
if num is not None:
num = num.group()
val = val.replace(num, f'{float(num)*factor:f}')
style.setProperty('font-size', val)
# We should also be dealing with the font shorthand property and
# font sizes specified as non numbers, but those are left as exercises
# for the reader

``main.py``를 분석해 보겠습니다. *글꼴 확대*라는 단일 도구를 정의하는 것을 볼 수 있습니다. 이 도구는 사용자에게 숫자를 요청하고 책의 모든 글꼴 크기를 해당 숫자로 곱합니다.

첫 번째 중요한 것은 도구 이름입니다. 이 도구의 키로 사용되므로 비교적 고유한 문자열로 설정해야 합니다.

다음 중요한 진입점은 :meth:`calibre.gui2.tweak_book.plugin.Tool.create_action`입니다. 이 메서드는 플러그인 도구 모음과 플러그인 메뉴에 나타나는 QAction 객체를 생성합니다. 또한 선택적으로 사용자가 맞춤 설정할 수 있는 키보드 단축키를 할당합니다. QAction의 triggered 시그널은 사용자에게 글꼴 크기 배수를 요청한 다음 확대 코드를 실행하는 ask_user() 메서드에 연결됩니다.

확대 코드는 주석이 잘 달려 있고 꽤 간단합니다. 주목할 점은 편집기 창의 참조를 ``self.gui``로, 편집기 *Boss*를 ``self.boss``로 얻는 것입니다. *Boss*는 편집기 사용자 인터페이스를 제어하는 객체입니다. calibre.gui2.tweak_book.boss.Boss 클래스에 문서화된 많은 유용한 메서드를 가지고 있습니다.

마지막으로, 편집 중인 책에 대한 참조인 ``self.current_container``가 있습니다. calibre.ebooks.oeb.polish.container.Container 객체입니다. 이것은 책을 구성하는 HTML/CSS/이미지 파일의 모음으로 표현하며, 많은 유용한 작업을 수행하는 편의 메서드를 가지고 있습니다. 컨테이너 객체와 플러그인 코드에서 재사용할 수 있는 다양한 유용한 유틸리티 함수는 :ref:`polish_api`에 문서화되어 있습니다.

플러그인에 번역 추가

플러그인의 모든 사용자 인터페이스 문자열을 Calibre 메인 사용자 인터페이스에 설정된 언어로 번역하여 표시할 수 있습니다.

첫 번째 단계는 플러그인의 소스 코드를 살펴보고 사용자에게 보이는 모든 문자열을 _()로 감싸 번역 가능하도록 표시하는 것입니다. 예:

action_spec = (_('My plugin'), None, _('My plugin is cool'), None)

그런 다음 프로그램을 사용하여 플러그인 소스 코드에서 .po 파일을 생성하세요. 번역하려는 각 언어마다 하나의 .po 파일이 있어야 합니다. 예: 독일어의 경우 de.po, 프랑스어의 경우 fr.po 등. 이를 위해 Poedit [https://poedit.net/] 프로그램을 사용할 수 있습니다.

이 .po 파일을 번역자에게 보내세요. 번역이 완료되면 .mo 파일로 컴파일하세요. Poedit를 다시 사용하거나 다음을 실행하세요:

calibre-debug -c "from calibre.translations.msgfmt import main; main()" filename.po

.mo 파일을 플러그인의 translations 폴더에 넣으세요.

마지막 단계는 플러그인의 .py 파일 상단에서 load_translations() 함수를 호출하는 것입니다. 성능상의 이유로 실제로 번역 가능한 문자열이 있는 .py 파일에서만 이 함수를 호출해야 합니다. 따라서 일반적인 사용자 인터페이스 플러그인에서는 ui.py 상단에서 호출하되 ``__init__.py``에서는 호출하지 않습니다.

calibre의 환경설정 → 인터페이스 → 모양과 느낌`에서 사용자 인터페이스 언어를 변경하거나 ``CALIBRE_OVERRIDE_LANG` 환경 변수를 설정하여 calibre를 실행하여 플러그인의 번역을 테스트할 수 있습니다. 예:

CALIBRE_OVERRIDE_LANG=de

``de``를 테스트하려는 언어의 언어 코드로 바꾸세요.

복수형이 있는 번역의 경우 _() 대신 ngettext() 함수를 사용하세요. 예:

ngettext('Delete a book', 'Delete {} books', num_books).format(num_books)

플러그인 API

위에서 눈치채셨겠지만, calibre의 플러그인은 클래스입니다. calibre에는 다양한 유형의 플러그인에 대한 서로 다른 클래스가 있습니다. 모든 플러그인의 기본 클래스를 포함한 각 클래스에 대한 세부 정보는 :ref:`plugins`에서 확인할 수 있습니다.

플러그인은 거의 확실히 calibre의 코드를 사용하게 됩니다. calibre 코드베이스에서 다양한 기능을 찾는 방법을 알아보려면 calibre 코드 구성 섹션을 읽어보세요.

플러그인 디버깅

첫 번째이자 가장 중요한 단계는 디버그 모드에서 calibre를 실행하는 것입니다. 명령줄에서 다음을 실행할 수 있습니다:

calibre-debug -g

You can have all the user interface strings in your plugin translated and displayed in whatever language is set for the main calibre user interface.

명령줄에서 실행할 경우 디버그 출력은 콘솔에 표시되고, Calibre 내에서 실행할 경우 출력은 텍스트 파일에 저장됩니다.

플러그인 코드 어디에나 print 문을 삽입할 수 있으며, 디버그 모드에서 출력됩니다. 기억하세요, 이것은 Python이므로 디버깅에 print 문 이상의 것이 필요하지 않습니다 ;) 저는 이 디버깅 기법만으로 calibre 전체를 개발했습니다.

다음 명령줄을 사용하면 플러그인 변경 사항을 빠르게 테스트할 수 있습니다:

calibre-debug -s; calibre-customize -b /path/to/your/plugin/folder; calibre

실행 중인 calibre를 종료하고, 종료가 완료될 때까지 기다린 다음, calibre에서 플러그인을 업데이트하고 calibre를 다시 시작합니다.

더 많은 플러그인 예시

많은 정교한 calibre 플러그인 목록은 `여기 <https://www.mobileread.com/forums/showthread.php?t=118764>`_에서 확인할 수 있습니다.

다른 사람과 플러그인 공유

만든 플러그인을 calibre의 다른 사용자와 공유하고 싶다면 `calibre 플러그인 포럼 <https://www.mobileread.com/forums/forumdisplay.php?f=237>`_에 새 스레드로 플러그인을 게시하세요.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 튜토리얼 »
- 전자책의 수학 조판하기

전자책의 수학 조판하기

캘리버 전자책 뷰어에는 전자책(EPUB 및 HTML 파일)에 포함된 수학을 표시하는 기능이 있습니다. TeX, MathML 또는 AsciiMath를 사용하여 수학을 직접 조판할 수 있습니다. 캘리버 전자책 뷰어는 뛰어난 MathJax [https://www.mathjax.org] 라이브러리를 사용하여 이를 수행합니다. 이 튜토리얼은 수학이 포함된 전자책을 만드는 간단한 튜토리얼로, 이 튜토리얼은 캘리버 전자책 뷰어와 잘 연동됩니다.

수학이 포함된 간단한 HTML 파일

간단한 HTML 파일 안에 수학을 인라인으로 작성할 수 있으며, 캘리버 전자책 뷰어는 이를 적절하게 조판된 수학으로 렌더링합니다. 아래 예에서는 수학에 TeX 표기법을 사용합니다. 앰퍼샌드와 보다 작거나 큰 부호는 각각 &amp; &lt; 및 &gt;로 적어야 하는 작은 경고와 함께 일반 TeX 명령을 사용할 수 있음을 알 수 있습니다.

첫 번째 단계는 Calibre에게 이 파일에 수학 공식이 포함될 것임을 알려주는 것입니다. 이를 위해 HTML 파일의 <head> 섹션에 다음 코드 조각을 추가합니다:

<script type="text/x-mathjax-config"></script>

자, 이제 .tex 파일에서처럼 수학식을 입력할 수 있습니다. 예를 들어, 로렌츠 방정식은 다음과 같습니다:

<h2>The Lorenz Equations</h2>

<p>
\begin{align}
\dot{x} &amp; = \sigma(y-x) \\
\dot{y} &amp; = \rho x - y - xz \\
\dot{z} &amp; = -\beta z + xy
\end{align}
</p>

이 토막글은 캘리버 전자책 뷰어에서 다음 스크린샷과 같습니다.

로렌츠 방정식

더 많은 방정식과 인라인 수학이 포함된 완전한 HTML 파일이 아래에 재현되어 있습니다. 당신은 이 HTML 파일을 EPUB로 변환하여 다른 사람에게 쉽게 배포할 수 있는 전자책을 만들 수 있습니다.

<!DOCTYPE html>
<html>
<!-- Copyright (c) 2012 Design Science, Inc. -->
<head>
<title>Math Test Page</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />

<!-- This script tag is needed to make calibre's ebook-viewer recognize that this file needs math typesetting -->
<script type="text/x-mathjax-config">
// This line adds numbers to all equations automatically, unless explicitly suppressed.
MathJax.tex = {tags: 'all'};
</script>

<style> h1 {text-align:center}
h2 { font-weight: bold; background-color: #DDDDDD; padding: .2em .5em; margin-top: 1.5em; border-top: 3px solid #666666; border-bottom: 2px solid #999999;
}
</style>
</head>
<body>

<h1>Sample Equations</h1>

<h2>The Lorenz Equations</h2>

<p> \begin{align}
\dot{x} &amp; = \sigma(y-x) \label{lorenz}\\
\dot{y} &amp; = \rho x - y - xz \\
\dot{z} &amp; = -\beta z + xy
\end{align} </p>

<h2>The Cauchy-Schwarz Inequality</h2>

<p>\[
\left( \sum_{k=1}^n a_k b_k \right)^{\!\!2} \leq
\left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)
\]</p>

<h2>A Cross Product Formula</h2>

<p>\[
\mathbf{V}_1 \times \mathbf{V}_2 =
\begin{vmatrix}
\mathbf{i} &amp; \mathbf{j} &amp; \mathbf{k} \\
\frac{\partial X}{\partial u} &amp; \frac{\partial Y}{\partial u} &amp; 0 \\
\frac{\partial X}{\partial v} &amp; \frac{\partial Y}{\partial v} &amp; 0 \\
\end{vmatrix}
\]</p>

<h2>The probability of getting \(k\) heads when flipping \(n\) coins is:</h2>

<p>\[P(E) = {n \choose k} p^k (1-p)^{ n-k} \]</p>

<h2>An Identity of Ramanujan</h2>

<p>\[
\frac{1}{(\sqrt{\phi \sqrt{5}}-\phi) e^{\frac25 \pi}} =
1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}}
{1+\frac{e^{-8\pi}} {1+\ldots} } } }
\]</p>

<h2>A Rogers-Ramanujan Identity</h2>

<p>\[
1 + \frac{q^2}{(1-q)}+\frac{q^6}{(1-q)(1-q^2)}+\cdots =
\prod_{j=0}^{\infty}\frac{1}{(1-q^{5j+2})(1-q^{5j+3})},
\quad\quad \text{for $|q|&lt;1$}.
\]</p>

<h2>Maxwell's Equations</h2>

<p> \begin{align}
\nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} &amp; = \frac{4\pi}{c}\vec{\mathbf{j}} \\
\nabla \cdot \vec{\mathbf{E}} &amp; = 4 \pi \rho \\
\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} &amp; = \vec{\mathbf{0}} \\
\nabla \cdot \vec{\mathbf{B}} &amp; = 0
\end{align} </p>

<h2>Inline Mathematics</h2>

<p>While display equations look good for a page of samples, the ability to mix math and text in a paragraph is also important. This expression \(\sqrt{3x-1}+(1+x)^2\) is an example of an inline equation. As you see, equations can be used this way as well, without unduly disturbing the spacing between lines.</p>

<h2>References to equations</h2>

<p>Here is a reference to the Lorenz Equations (\ref{lorenz}). Clicking on the equation number will take you back to the equation.</p>

</body>
</html>

참고

Calibre 전자책 뷰어는 MathML과 TeX를 모두 지원하지만, MathML이 제대로 표시되려면 HTML 파일에 ``<script type=”text/x-mathjax-config”></script>``줄을 반드시 포함해야 합니다.

자세한 정보

캘리버 전자책 뷰어는 MathJax 라이브러리를 사용하여 수학을 렌더링하므로 전자책에서 수학에 대해 자세히 알아보고 도움을 얻을 수 있는 가장 좋은 곳은 `MathJax 웹사이트 <https://www.mathjax.org>`_입니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 튜토리얼 »
- AZW3 • EPUB • MOBI 카탈로그 만들기

AZW3 • EPUB • MOBI 카탈로그 만들기

캘리버의 카탈로그 만들기 기능을 사용하면 다양한 형식으로 라이브러리의 카탈로그를 만들 수 있습니다. 이 도움말 파일은 AZW3, EPUB, MOBI 형식으로 카탈로그를 생성할 때의 카탈로그 작성 옵션을 설명합니다.

- 카탈로그에 포함할 책 선택

- 포함된 섹션

- 접두사

- 제외된 책

- 제외된 장르

- 기타 옵션

- 사용자 정의 카탈로그 표지

- 추가 도움말 자료

카탈로그에 포함할 책 선택

라이브러리의 모든 책을 카탈로그에 포함하려면 기본 창에서 검색 또는 필터링 기준을 모두 제거하세요. 한 권의 책만 선택된 상태에서 라이브러리의 모든 책이 생성될 카탈로그에 포함될 후보가 됩니다. 개별 책은 다양한 기준으로 제외할 수 있습니다. 자세한 내용은 아래 제외된 장르 섹션을 참조하세요.

라이브러리의 일부 책만 카탈로그에 포함하려면 두 가지 방법이 있습니다:

- 카탈로그에 포함할 책을 여러 개 선택하세요. 캘리버의 기본 창에서 두 권 이상의 책을 선택하면 선택한 책만 카탈로그에 포함됩니다.

- 검색 필드 또는 태그 브라우저를 사용하여 표시된 책을 필터링하세요. 표시된 책만 카탈로그에 포함됩니다.

카탈로그 생성을 시작하려면 메뉴 항목 책 변환 > 캘리버 라이브러리의 책 카탈로그 만들기`를 선택하세요. 카탈로그 만들기 대화상자에 더 쉽게 접근할 수 있도록 :guilabel:`환경설정 > 인터페이스 > 도구 모음 및 메뉴`에서 도구 모음에 :guilabel:`카탈로그 만들기 버튼을 추가할 수도 있습니다.

카탈로그 옵션`에서 카탈로그 형식으로 **AZW3, EPUB 또는 MOBI**를 선택하세요. :guilabel:`카탈로그 제목 필드에 생성될 카탈로그에 사용할 이름을 입력하세요. 같은 이름과 형식의 카탈로그가 이미 존재하면 새로 생성된 카탈로그로 교체됩니다.

:guilabel:`카탈로그를 장치에 자동으로 보내기`를 활성화하면 완료 시 생성된 카탈로그가 연결된 장치에 다운로드됩니다.

포함된 섹션

체크 표시로 활성화된 섹션은 생성된 카탈로그에 포함됩니다:

- 저자 - 모든 책이 저자순으로 정렬되어 목록 형태로 표시됩니다. 시리즈가 아닌 책이 시리즈 책보다 먼저 나열됩니다.

- 제목 - 모든 책이 제목순으로 정렬되어 목록 형태로 표시됩니다.

- 시리즈 - 시리즈에 속한 모든 책이 시리즈순으로 정렬되어 목록 형태로 표시됩니다.

- 장르 - 개별 장르가 저자 및 시리즈순으로 정렬되어 목록으로 표시됩니다.

- 최근 추가됨 - 모든 책이 시간 역순으로 정렬됩니다. 최근 30일 이내에 추가된 책이 먼저 표시되고, 그 이후에 월별로 추가된 책 목록이 포함됩니다.

- 설명 - 표지 썸네일과 설명을 포함한 각 책의 상세 설명 페이지입니다. 저자순으로 정렬되며, 시리즈가 아닌 책이 시리즈 책보다 먼저 나열됩니다.

접두사

접두사 규칙을 사용하면 특정 기준이 충족될 때 책 목록에 접두사를 추가할 수 있습니다. 예를 들어, 읽은 책에 체크 표시를 하거나 읽고 싶은 목록의 책에 X 표시를 할 수 있습니다.

첫 번째 열의 체크박스는 규칙을 활성화합니다. 이름`은 사용자가 지정하는 규칙 이름입니다. :guilabel:`필드`는 :guilabel:`태그 또는 라이브러리의 사용자 정의 열입니다. :guilabel:`값`은 일치시킬 :guilabel:`필드`의 내용입니다. 접두사 규칙이 충족되면 해당 책에 선택한 :guilabel:`접두사`가 표시됩니다.

위 예시에서는 세 개의 접두사 규칙이 지정되었습니다:

1. :guilabel:`읽은 책`은 사용자 정의 열 :guilabel:`마지막으로 읽은 날짜`에 날짜가 있는 책에 체크 표시를 붙이도록 지정합니다.

2. 읽고 싶은 목록 항목은 읽고 싶은 목록 태그가 있는 책에 X 표시를 붙이도록 지정합니다.

3. 라이브러리 책은 사용자 정의 열 :guilabel:`라이브러리에 보유`의 값이 참(또는 Yes)인 책에 양방향 화살표 표시를 붙이도록 지정합니다.

첫 번째로 일치하는 규칙이 접두사를 결정합니다. 비활성화되거나 불완전한 규칙은 무시됩니다.

제외된 책

제외 규칙을 사용하면 카탈로그에 포함되지 않을 책을 지정할 수 있습니다.

첫 번째 열의 체크박스는 규칙을 활성화합니다. 이름`은 사용자가 지정하는 규칙 이름입니다. :guilabel:`필드`는 :guilabel:`태그 또는 라이브러리의 사용자 정의 열입니다. :guilabel:`값`은 일치시킬 :guilabel:`필드`의 내용입니다. 제외 규칙이 충족되면 해당 책은 생성된 카탈로그에서 제외됩니다.

위 예시에서는 두 개의 제외 규칙이 지정되었습니다:

1. 카탈로그 규칙은 카탈로그 태그가 있는 모든 책이 생성된 카탈로그에서 제외되도록 지정합니다.

2. 보관됨 책 규칙은 사용자 정의 열 :guilabel:`상태`의 값이 :guilabel:`보관됨`인 모든 책이 생성된 카탈로그에서 제외되도록 지정합니다.

모든 규칙은 각 책에 대해 평가됩니다. 비활성화되거나 불완전한 규칙은 무시됩니다.

제외된 장르

카탈로그가 생성될 때 데이터베이스의 태그가 장르로 사용됩니다. 예를 들어 소설``과 ``비소설 태그를 사용할 수 있습니다. 이 태그는 생성된 카탈로그에서 장르가 되며, 책은 지정된 태그에 따라 해당 장르 목록 아래에 나열됩니다. 책은 해당하는 태그가 있는 모든 장르 섹션에 나열됩니다.

특정 태그를 다른 목적으로 사용하는 경우가 있습니다. 예를 들어 +로 읽은 책을 표시하거나 [Amazon Freebie]``와 같은 괄호 태그로 책의 출처를 나타낼 수 있습니다. :guilabel:`제외할 장르` 정규 표현식을 사용하면 생성된 카탈로그에서 장르로 사용하지 않을 태그를 지정할 수 있습니다. 기본 제외 정규 표현식 패턴 ``\[.+\]\+``은 ``[tag] 형태의 태그와 읽은 책의 기본 태그인 ``+``를 생성된 카탈로그에서 장르로 사용되지 않도록 제외합니다.

정규 표현식에서 정확한 태그 이름을 사용할 수도 있습니다. 예를 들어 [Amazon Freebie] 또는 [Project Gutenberg]. 여러 정확한 태그를 제외 목록에 추가하려면 파이프(|) 문자로 구분하세요: [Amazon Freebie]|[Project Gutenberg].

:guilabel:`정규 표현식 결과`는 데이터베이스의 태그와 입력한 정규 표현식 패턴을 기반으로 카탈로그가 빌드될 때 어떤 태그가 제외되는지 보여줍니다. 정규 표현식 패턴을 수정하면 결과가 업데이트됩니다.

기타 옵션

:guilabel:`카탈로그 표지`는 새 표지를 생성할 것인지 기존 표지를 사용할 것인지 지정합니다. 카탈로그에 사용자 정의 표지를 만들 수 있습니다 - 자세한 내용은 :ref:`custom_catalog_covers`를 참조하세요. 재사용할 사용자 정의 표지를 만든 경우 :guilabel:`기존 표지 사용`을 선택하세요. 그렇지 않으면 :guilabel:`새 표지 생성`을 선택하세요.

추가 설명 메모`는 설명 페이지의 표지 썸네일 옆에 삽입할 사용자 정의 열의 내용을 지정합니다. 예를 들어 :guilabel:`마지막으로 읽은 날짜 사용자 정의 열을 사용하여 마지막으로 책을 읽은 날짜를 표시할 수 있습니다. 설명 메모 기능의 고급 사용법은 `캘리버 포럼의 이 게시물 <https://www.mobileread.com/forums/showpost.php?p=1335767&postcount=395>`_을 참조하세요.

:guilabel:`썸네일 너비`는 설명 페이지에 포함될 표지 썸네일의 너비 기본값을 지정합니다. 썸네일은 성능 향상을 위해 캐시됩니다. 다양한 너비를 실험해 보려면 원하는 너비를 결정할 때까지 몇 권의 책만으로 카탈로그를 생성해 본 다음 전체 카탈로그를 생성하세요. 새로운 썸일 너비로 처음 카탈로그를 생성하면 성능이 느려지지만, 이후 카탈로그 빌드는 썸네일 캐시를 활용합니다.

설명과 병합`은 카탈로그 생성 중 설명 메타데이터와 비파괴적으로 병합될 사용자 정의 열의 내용을 지정합니다. 예를 들어 설명 메타데이터에 추가하려는 :guilabel:`저자 소개 사용자 정의 열이 있을 수 있습니다. 사용자 정의 열 내용을 설명 섹션 앞이나 뒤에 삽입하도록 선택할 수 있으며, 선택적으로 추가된 내용을 수평선 구분 기호로 구분할 수 있습니다. 사용 가능한 사용자 정의 열 유형에는 ``텍스트, 설명, 복합``이 포함됩니다.

사용자 정의 카탈로그 표지

`Generate Cover 플러그인 <https://www.mobileread.com/forums/showthread.php?t=124219>`_을 설치하면 카탈로그에 사용자 정의 표지를 만들 수 있습니다. 플러그인을 설치하려면 :guilabel:`환경설정 > 고급 > 플러그인 > 새 플러그인 가져오기`로 이동하세요.

추가 도움말 자료

캘리버 카탈로그 기능에 대한 자세한 내용은 MobileRead 포럼 고정 게시글 `카탈로그 만들기 - 여기서 시작하세요 <https://www.mobileread.com/forums/showthread.php?t=118556>`_를 참조하세요. 카탈로그 템플릿을 사용자 정의하는 방법과 버그 보고서를 제출하는 방법에 대한 정보를 찾을 수 있습니다.

질문하거나 다른 사용자와 캘리버 카탈로그 기능에 대해 논의하려면 MobileRead 포럼 `라이브러리 관리 <https://www.mobileread.com/forums/forumdisplay.php?f=236>`_를 방문하세요.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 튜토리얼 »
- 가상 라이브러리

가상 라이브러리

캘리버에서 가상 라이브러리는 일반 라이브러리의 일부만 열도록 캘리버에 지시하는 방법입니다. 예를 들어, 특정 저자의 책만 작업하거나 특정 태그가 있는 책만 작업하고 싶을 수 있습니다. 가상 라이브러리를 사용하는 것은 큰 책 컬렉션을 더 작은 하위 컬렉션으로 분할하는 선호되는 방법입니다. 전체 컬렉션을 검색하고 싶을 때 전체 라이브러리로 간단히 돌아갈 수 있으므로 라이브러리를 여러 개의 작은 라이브러리로 분할하는 것보다 우수합니다. 캘리버에서 여러 개의 별도 라이브러리를 동시에 검색하는 방법은 없습니다.

가상 라이브러리는 단순한 검색과 다릅니다. 검색은 책 목록에 표시되는 책 목록만 제한합니다. 가상 라이브러리는 그렇게 할 뿐만 아니라, 왼쪽의 :guilabel:`태그 브라우저`에 표시되는 항목도 제한합니다. 태그 브라우저는 가상 라이브러리의 책에서 나온 태그, 저자, 시리즈, 출판사 등만 표시합니다. 따라서 가상 라이브러리는 실제 라이브러리가 제한된 책 세트만 포함하는 것처럼 동작합니다.

가상 라이브러리 만들기

가상 라이브러리를 사용하려면 검색 표시줄 왼쪽에 있는 가상 라이브러리 버튼을 클릭하고 가상 라이브러리 만들기 옵션을 선택하세요. 첫 번째 예시로, 특정 저자의 책만 보여주는 가상 라이브러리를 만들어 보겠습니다. 아래 그림과 같이 저자 링크를 클릭하고 사용할 저자를 선택한 다음 확인을 클릭하세요.

가상 라이브러리 만들기 대화상자가 자동으로 채워졌습니다. 확인을 클릭하면 새 가상 라이브러리가 생성되고 자동으로 전환되어 선택한 저자의 책만 표시됩니다. 캘리버 측에서는 라이브러리에 선택한 저자의 책만 포함되어 있는 것처럼 취급됩니다.

:guilabel:`가상 라이브러리`를 다시 클릭하고 :guilabel:`<없음>`이라는 항목을 선택하여 언제든지 전체 라이브러리로 다시 전환할 수 있습니다.

가상 라이브러리는 *검색*을 기반으로 합니다. 검색을 가상 라이브러리의 기반으로 사용할 수 있습니다. 가상 라이브러리는 해당 검색과 일치하는 책만 포함합니다. 먼저, 검색 표시줄에 사용할 검색어를 입력하거나 태그 브라우저`를 사용하여 검색을 구성하세요. 반환된 결과에 만족하면 :guilabel:`가상 라이브러리 버튼을 클릭하고 :guilabel:`라이브러리 만들기`를 선택한 다음 새 가상 라이브러리의 이름을 입력하세요. 그러면 방금 입력한 검색을 기반으로 가상 라이브러리가 생성됩니다. 검색은 매우 강력하며, 검색으로 수행할 수 있는 작업의 예는 :ref:`search_interface`를 참조하세요.

유용한 가상 라이브러리 예시

- 지난 1일 동안 캘리버에 추가된 책:

date:>1daysago

- 지난 한 달 동안 캘리버에 추가된 책:

date:>30daysago

- 평점이 별 5개인 책:

rating:5

- 평점이 별 4개 이상인 책:

rating:>=4

- 평점이 없는 책:

rating:false

- 캘리버의 뉴스 가져오기 기능으로 다운로드한 정기간행물:

tags:=뉴스 and author:=calibre

- 태그가 없는 책:

tags:false

- 표지가 없는 책:

cover:false

가상 라이브러리 작업

:guilabel:`가상 라이브러리`를 클릭하고 적절한 동작을 선택하여 이전에 만든 가상 라이브러리를 편집하거나 제거할 수 있습니다.

:guilabel:`환경설정->인터페이스->동작`으로 이동하여 현재 라이브러리를 열 때 항상 특정 가상 라이브러리를 적용하도록 캘리버에 지시할 수 있습니다.

가상 라이브러리 버튼을 클릭하고 *현재 검색 항목을 선택하여 현재 검색을 임시 가상 라이브러리로 빠르게 사용할 수 있습니다.

사용 가능한 모든 가상 라이브러리를 책 목록 위의 탭으로 표시할 수 있습니다. 이는 가상 라이브러리 간에 자주 전환하는 경우에 특히 편리합니다. 가상 라이브러리 버튼을 클릭하고 :guilabel:`가상 라이브러리를 탭으로 표시`를 선택하세요. 탭을 끌어다 놓아 재배치하고 보고 싶지 않은 탭을 닫을 수 있습니다. 닫힌 탭은 탭 표시줄을 마우스 오른쪽 버튼으로 클릭하여 복원할 수 있습니다.

검색에서 가상 라이브러리 사용하기
vl: 접두사를 사용하여 가상 라이브러리에 있는 책을 검색할 수 있습니다. 예를 들어, vl:읽음``은 *읽음* 가상 라이브러리의 모든 책을 찾습니다. ``vl:읽음 and vl:"공상 과학" 검색은 읽음*과 *공상 과학 가상 라이브러리 모두에 있는 모든 책을 찾습니다.

vl: 뒤의 값은 가상 라이브러리의 이름이어야 합니다. 가상 라이브러리 이름에 공백이 있으면 따옴표로 감싸세요.

가상 라이브러리 검색의 한 가지 용도는 콘텐츠 서버입니다. :guilabel:`환경설정->네트워크 공유->사용자 이름과 비밀번호 필요`에서 사용자에게 보이는 캘리버 라이브러리를 제한할 수 있습니다. 각 보이는 라이브러리에 대해 검색 표현식을 지정하여 표시되는 책을 더 제한할 수 있습니다. ``vl:”가상 라이브러리 이름”``을 사용하여 가상 라이브러리에 있는 책으로 제한하세요.

추가 제한 사용하기

추가 제한`을 사용하여 가상 라이브러리에 표시되는 책을 더 제한할 수 있습니다. 추가 제한은 이전에 만든 저장된 검색으로, 현재 가상 라이브러리에 적용하여 가상 라이브러리에 표시되는 책을 더 제한할 수 있습니다. 예를 들어, :guilabel:`역사 소설`로 태그된 책에 대한 가상 라이브러리와 읽지 않은 책을 보여주는 저장된 검색이 있다면, :guilabel:`가상 라이브러리 버튼을 클릭하고 추가 제한 옵션을 선택하여 읽지 않은 역사 소설만 표시할 수 있습니다. 저장된 검색에 대해 알아보려면 :ref:`saved_searches`를 참조하세요.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- calibre:// URL 스키마

calibre:// URL 스키마

캘리버는 calibre:// URL의 처리 프로그램으로 스스로를 등록합니다. 따라서 다른 프로그램/문서에서 또는 명령줄을 통해 책 열기, 책 검색 등의 작업을 수행하는 데 사용할 수 있습니다. 예를 들어 명령줄에서 다음을 실행하면:

calibre calibre://switch-library/Some_Library

``Some Library``라는 이름의 라이브러리로 캘리버를 엽니다. 라이브러리 이름은 라이브러리 폴더의 폴더 이름에서 공백이 밑줄로 바뀐 것입니다. 특수 값 ``_``는 현재 라이브러리를 의미합니다. 다양한 유형의 URL은 아래에 문서화되어 있습니다.

HTML 파일이나 Word 문서 등에 이러한 링크를 넣을 수도 있으며, 운영 체제가 자동으로 캘리버를 실행하여 지정된 작업을 수행합니다.

- 특정 라이브러리로 전환

- 캘리버에서 특정 책 표시

- 전자책 뷰어에서 특정 위치의 특정 책 열기

- 책 검색

- 특정 라이브러리의 책에 대한 책 세부정보 창 열기

- 저자/시리즈 등과 연결된 메모 열기

- URL 매개변수의 16진수 인코딩

특정 라이브러리로 전환

URL 구문은 다음과 같습니다:

calibre://switch-library/Library_Name

라이브러리 이름은 라이브러리의 폴더 이름에서 공백이 밑줄로 바뀐 것입니다. 특수 값 ``_``는 현재 라이브러리를 의미합니다. 라이브러리 이름에 :ref:`16진수 인코딩 <hex_encoding>`을 사용할 수도 있는데, 라이브러리 이름에 URL 인코딩이 필요한 특수 문자가 있는 경우 유용합니다. 16진수로 인코딩된 라이브러리 이름은 다음과 같습니다:

_hex_-AD23F4BC

여기서 _hex_- 접두사 뒤의 부분은 UTF-8로 인코딩된 라이브러리 이름이며 각 바이트는 두 개의 16진수 문자로 표현됩니다.

캘리버에서 특정 책 표시

URL 구문은 다음과 같습니다:

calibre://show-book/Library_Name/book_id

이렇게 하면 캘리버에서 ``book_id``(숫자)에 해당하는 책이 표시됩니다. 책의 ID는 캘리버 인터페이스에서 책 세부정보 패널의 클릭하여 열기 링크 위에 마우스를 올리면 볼 수 있으며, 책 폴더 경로 끝의 괄호 안에 있는 숫자입니다.

책 세부정보 패널을 마우스 오른쪽 버튼으로 클릭하고 :guilabel:`책 링크 복사`를 선택하여 캘리버에 표시된 현재 책의 링크를 복사할 수 있습니다.

검색이 활성 상태이고 해당 책이 검색과 일치하지 않으면 검색이 지워집니다.

가상 라이브러리가 선택되어 있으면 캘리버는 책을 표시할 때 이를 사용합니다. 해당 가상 라이브러리에서 책을 찾을 수 없으면 가상 라이브러리가 지워집니다.

책을 표시할 때 특정 가상 라이브러리로 전환하려면 다음을 사용하세요:

calibre://show-book/Library_Name/book_id?virtual_library=Library%20Name
or
calibre://show-book/Library_Name/book_id?encoded_virtual_library=hex_encoded_virtual_library_name

가상 라이브러리 이름의 공백을 ``%20``으로 바꾸세요. 해당 가상 라이브러리에서 책을 찾을 수 없으면 가상 라이브러리가 무시됩니다.

전자책 뷰어에서 특정 위치의 특정 책 열기

URL 구문은 다음과 같습니다:

calibre://view-book/Library_Name/book_id/book_format?open_at=location

여기서 book_format``은 책의 형식(예: ``EPUB 또는 MOBI)이고 ``location``은 책 내부의 선택적 위치입니다. 이러한 링크를 얻는 가장 쉬운 방법은 뷰어에서 책을 연 다음 뷰어 컨트롤에서 :guilabel:`이동->위치`를 선택하면 다른 곳에 복사/붙여넣기할 수 있는 링크가 제공됩니다.

책 검색

URL 구문은 다음과 같습니다:

calibre://search/Library_Name?q=query
calibre://search/Library_Name?eq=hex_encoded_query

여기서 쿼리는 유효한 :ref:`검색 표현식 <search_interface>`입니다. 검색 표현식이 복잡하면 :ref:`16진수 문자열로 인코딩 <hex_encoding>`하고 대신 ``eq``를 사용하세요. 쿼리를 생략하면 현재 검색이 지워집니다.

기본적으로 가상 라이브러리가 선택되어 있으면 캘리버는 검색을 수행하기 전에 이를 지워 모든 책이 검색되도록 합니다. 가상 라이브러리를 유지하려면 다음을 사용하세요:

calibre://search/Library_Name?q=query&virtual_library=_

특정 가상 라이브러리로 전환하려면 다음을 사용하세요:

calibre://search/Library_Name?virtual_library=Library%20Name
or
calibre://search/Library_Name?encoded_virtual_library=hex_encoded_virtual_library_name

가상 라이브러리 이름의 공백을 ``%20``으로 바꾸세요.

캘리버에서 검색을 수행하고 링크를 생성하려면 검색 표시줄을 마우스 오른쪽 버튼으로 클릭하고 :guilabel:`검색을 URL로 복사`를 선택하면 됩니다.

특정 라이브러리의 책에 대한 책 세부정보 창 열기

URL 구문은 다음과 같습니다:

calibre://book-details/Library_Name/book_id

현재 라이브러리나 선택한 책을 변경하지 않고 지정된 라이브러리의 지정된 책에 대한 책 세부정보 창을 엽니다.

저자/시리즈 등과 연결된 메모 열기

URL 구문은 다음과 같습니다:

calibre://show-note/Library_Name/Field_Name/id_Item_Id

이렇게 하면 지정된 항목의 메모를 보여주는 창이 열립니다. 이러한 URL을 만드는 가장 쉬운 방법은 캘리버에서 원하는 메모를 표시하고 URL 복사 버튼을 클릭하여 URL을 클립보드에 복사한 다음 필요한 곳에 붙여넣는 것입니다.

여기서 Field_Name``은 ``authors``나 ``tags 같은 열의 이름입니다. 사용자가 만든 열의 경우 필드 이름의 앞에 오는 ``#``을 밑줄로 바꾸세요. 예를 들어 ``#mytags``는 ``_mytags``가 됩니다.

Item_Id``를 사용하여 ID로 항목을 지정하는 것 외에도 ``val_Item_Name 또는 ``hex_Hex_Encoded_Item_Name``을 사용하여 이름으로 지정할 수도 있습니다. 예를 들어:

calibre://book-details/Library_Name/authors/val_John%20Doe

URL 매개변수의 16진수 인코딩

URL 매개변수의 16진수 인코딩은 먼저 매개변수를 UTF-8 바이트로 인코딩한 다음 각 바이트를 해당 바이트를 나타내는 두 개의 16진수 문자로 바꾸는 방식으로 수행됩니다. 예를 들어 문자열 ``abc``는 UTF-8에서 바이트 ``0x61 0x62 0x63``이므로 인코딩된 버전은 문자열 ``616263``입니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 캘리버 사용자 정의

캘리버 사용자 정의

캘리버는 매우 모듈화된 설계를 가지고 있습니다. 다양한 부분을 사용자 정의할 수 있습니다. 여기서 다음 내용을 배우게 됩니다:

- 환경 변수와 *트윅*을 사용하여 캘리버의 동작을 사용자 정의하는 방법,

- 아이콘과 템플릿 같은 자신만의 정적 리소스를 지정하여 기본값을 재정의하는 방법

- *플러그인*을 사용하여 캘리버에 기능을 추가하는 방법.

- 다른 캘리버 사용자와 아이콘 테마 및 플러그인을 공유하는 방법.

- 캘리버에 새로운 온라인 콘텐츠 소스를 추가하기 위한 *레시피*를 만드는 방법은 즐겨찾는 뉴스 웹사이트 추가 섹션을 방문하세요.

참고

아이콘 테마와 플러그인은 캘리버의 내장된 업데이터를 통해 인덱싱되고 다운로드할 수 있지만 캘리버의 일부가 아니며 지원 및 소스 코드에 대한 표준 위치는 지원 스레드의 `Mobileread 포럼 <https://www.mobileread.com/forums/forumdisplay.php?f=166>`__에 있습니다.

- 환경 변수

- 트윅

- 아이콘, 템플릿 등 재정의

- 캘리버용 사용자 정의 아이콘 테마 만들기

- 플러그인으로 캘리버 사용자 정의

환경 변수

- CALIBRE_CONFIG_DIRECTORY - 구성 파일이 저장/읽기되는 폴더를 설정합니다.

- CALIBRE_TEMP_DIR - 캘리버가 사용하는 임시 폴더를 설정합니다

- CALIBRE_CACHE_DIRECTORY - 캘리버가 세션 간에 영속 데이터를 캐시하는 폴더를 설정합니다

- CALIBRE_OVERRIDE_DATABASE_PATH - metadata.db의 전체 경로를 지정할 수 있습니다. 이 변수를 사용하면 metadata.db를 라이브러리 폴더가 아닌 다른 위치에 둘 수 있습니다. 라이브러리 폴더가 파일 잠금을 지원하지 않는 네트워크 드라이브에 있는 경우에 유용합니다.

- CALIBRE_ALLOW_PYTHON_TEMPLATES - 1 이외의 값으로 설정하면 캘리버에서 Python 기반 템플릿의 사용을 비활성화합니다.

- CALIBRE_DEVELOP_FROM - 캘리버 개발 환경에서 실행하는 데 사용합니다. :ref:`develop`을 참조하세요.

- CALIBRE_OVERRIDE_LANG - 인터페이스에서 사용하는 언어를 강제하는 데 사용합니다 (ISO 639 언어 코드)

- CALIBRE_TEST_TRANSLATION - 번역 .po 파일을 테스트하는 데 사용합니다 (.po 파일의 경로여야 합니다)

- CALIBRE_NO_NATIVE_FILEDIALOGS - 캘리버가 파일/폴더 선택 시 기본 파일 대화상자를 사용하지 않도록 합니다.

- CALIBRE_NO_NATIVE_MENUBAR - Ubuntu Unity 및 유사한 Linux 데스크톱 환경에서 캘리버가 기본(전역) 메뉴를 생성하지 않도록 합니다. 대신 메뉴가 기존 방식대로 창 안에 배치됩니다.

- CALIBRE_USE_SYSTEM_THEME - 기본적으로 Linux에서 캘리버는 자체 내장 Qt 스타일을 사용합니다. 이는 캘리버가 빌드된 Qt 버전과 시스템 Qt 간의 비호환성으로 인한 충돌 및 멈춤을 방지하기 위함입니다. 단점은 캘리버가 시스템의 모양과 느낌을 따르지 않을 수 있다는 것입니다. Linux에서 이 환경 변수를 설정하면 캘리버가 시스템 테마를 사용하게 됩니다 – 충돌 및 멈춤에 주의하세요.

- CALIBRE_SHOW_DEPRECATION_WARNINGS - 캘리버가 stdout에 사용 중단 경고를 출력하도록 합니다. 캘리버 개발자에게 유용합니다.

- CALIBRE_NO_DEFAULT_PROGRAMS - 캘리버가 처리 가능한 파일 형식을 Windows에 자동으로 등록하지 않도록 방지합니다.

- CALIBRE_USE_SYSTEM_CERTIFICATES - Windows 및 macOS에서 캘리버가 자체 인증서 저장소 대신 시스템 인증서 저장소를 사용하여 SSL 인증서 검증을 수행하도록 합니다.

- CALIBRE_NO_ICONS_IN_MENUS - 메뉴에서 아이콘 비활성화

- QT_QPA_PLATFORM - Linux에서 ``wayland``로 설정하면 캘리버가 Wayland를 사용하도록 강제하고, ``xcb``로 설정하면 X11을 사용하도록 강제합니다.

- SYSFS_PATH - sysfs가 /sys 외의 다른 곳에 마운트된 경우 사용

- http_proxy, https_proxy - Linux에서 HTTP(S) 프록시를 지정하는 데 사용

Windows에서 환경 변수를 설정하는 방법 <https://www.computerhope.com/issues/ch000549.htm>`_을 참조하세요. macOS를 사용하는 경우 :file:`~/Library/Preferences/calibre/macos-env.txt 파일을 생성하고 환경 변수를 한 줄에 하나씩 넣어 설정할 수 있습니다. 예를 들어:

CALIBRE_DEVELOP_FROM=$HOME/calibre-src/src
CALIBRE_NO_NATIVE_FILEDIALOGS=1
CALIBRE_CONFIG_DIRECTORY=~/.config/calibre

트윅

트윅은 캘리버의 동작을 다양한 측면에서 제어하기 위해 지정할 수 있는 작은 변경 사항입니다. 환경설정->고급->트윅으로 이동하여 변경할 수 있습니다. 트윅의 기본값은 아래에 나열되어 있습니다

#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
# License: GPLv3 Copyright: 2010, Kovid Goyal <kovid at kovidgoyal.net>

# Contains various tweaks that affect calibre behavior. Only edit this file if
# you know what you are doing. If you delete this file, it will be recreated from
# defaults.

#: Auto increment series index
# The algorithm used to assign a book added to an existing series a series number.
# New series numbers assigned using this tweak are always integer values, except
# if a constant non-integer is specified.
# Possible values are:
# next - First available integer larger than the largest existing number
# first_free - First available integer larger than 0
# next_free - First available integer larger than the smallest existing number
# last_free - First available integer smaller than the largest existing number. Return largest existing + 1 if no free number is found
# const - Assign the number 1 always
# no_change - Do not change the series index
# a number - Assign that number always. The number is not in quotes. Note that 0.0 can be used here.
# Examples:
# series_index_auto_increment = 'next'
# series_index_auto_increment = 'next_free'
# series_index_auto_increment = 16.5
#
# Set the use_series_auto_increment_tweak_when_importing tweak to True to
# use the above values when importing/adding books. If this tweak is set to
# False (the default) then the series number will be set to 1 if it is not
# explicitly set during the import. If set to True, then the
# series index will be set according to the series_index_auto_increment setting.
# Note that the use_series_auto_increment_tweak_when_importing tweak is used
# only when a value is not provided during import. If the importing regular
# expression produces a value for series_index, or if you are reading metadata
# from books and the import plugin produces a value, then that value will
# be used irrespective of the setting of the tweak.
series_index_auto_increment = 'next'
use_series_auto_increment_tweak_when_importing = False

#: Add separator after completing an author name
# Set this if the completion separator should be appended to the end of the
# completed text to automatically begin a new completion operation for authors.
# It can be either True or False authors_completer_append_separator = False

#: Author sort name algorithm
# The algorithm used to copy author to author_sort.
# Possible values are:
# invert: use "fn ln" -> "ln, fn"
# copy : copy author to author_sort without modification
# comma : use 'copy' if there is a ',' in the name, otherwise use 'invert'
# nocomma : "fn ln" -> "ln fn" (without the comma)
# When this tweak is changed, the author_sort values stored with each author
# must be recomputed by right-clicking on an author in the left-hand tags
# panel, selecting 'Manage authors', and pressing
# 'Recalculate all author sort values'.
#
# The author_name_suffixes are words that are ignored when they occur at the
# end of an author name. The case of the suffix is ignored and trailing
# periods are automatically handled.
#
# The same is true for author_name_prefixes.
#
# The author_name_copywords are a set of words which, if they occur in an
# author name, cause the automatically generated author sort string to be
# identical to the author's name. This means that the sort for a string like
# "Acme Inc." will be "Acme Inc." instead of "Inc., Acme".
#
# If author_use_surname_prefixes is enabled, any of the words in
# author_surname_prefixes will be treated as a prefix to the surname, if they
# occur before the surname. So for example, "John von Neumann" would be sorted
# as "von Neumann, John" and not "Neumann, John von".
author_sort_copy_method = 'comma'
author_name_suffixes = ('Jr', 'Sr', 'Inc', 'Ph.D', 'Phd',
'MD', 'M.D', 'I', 'II', 'III', 'IV',
'Junior', 'Senior')
author_name_prefixes = ('Mr', 'Mrs', 'Ms', 'Dr', 'Prof')
author_name_copywords = (
'Agency', 'Corporation', 'Company', 'Co.', 'Council',
'Committee', 'Inc.', 'Institute', 'National', 'Society', 'Club', 'Team',
'Software', 'Games', 'Entertainment', 'Media', 'Studios',
)
author_use_surname_prefixes = False author_surname_prefixes = ('da', 'de', 'di', 'la', 'le', 'van', 'von')

#: Splitting multiple author names
# By default, calibre splits a string containing multiple author names on
# ampersands and the words "and" and "with". You can customize the splitting
# by changing the regular expression below. Strings are split on whatever the
# specified regular expression matches, in addition to ampersands.
# Default: r'(?i),?\s+(and|with)\s+'
authors_split_regex = r'(?i),?\s+(and|with)\s+'

#: Use author_sort and/or series_sort for names in Tag browser
# Set which author or series field is used to display as the item name in the
# Tag browser (the list of authors, series, publishers etc on the left hand
# side). The choices for author are 'author' and 'author_sort'. The choices for
# series are 'series' and 'series_sort'. This tweak affects only what is
# displayed under the category in the Tag browser and Content server. Please
# note that if you set this to …_sort, it is possible to see duplicate names in
# the list because although it is guaranteed that author and series names are
# unique, there is no such guarantee for their sort values. Showing duplicates
# won't break anything but it could lead to some confusion. The tooltip for an
# item will show the item's name no matter how you set this tweak.
# Examples:
# categories_use_field_for_author_name = 'author'
# categories_use_field_for_author_name = 'author_sort'
# categories_use_field_for_series_name = 'series'
# categories_use_field_for_series_name = 'series_sort'
categories_use_field_for_author_name = 'author'
categories_use_field_for_series_name = 'series'

#: Control partitioning of Tag browser
# When partitioning the Tag browser, the format of the subcategory label is
# controlled by a template: categories_collapsed_name_template if sorting by
# name, categories_collapsed_rating_template if sorting by average rating, and
# categories_collapsed_popularity_template if sorting by popularity. There are
# two variables available to the template: first and last. The variable 'first'
# is the initial item in the subcategory, and the variable 'last' is the final
# item in the subcategory. Both variables are 'objects'; they each have multiple
# values that are obtained by using a suffix. For example, first.name for an
# author category will be the name of the author. The sub-values available are:
# name: the printable name of the item
# count: the number of books that references this item
# avg_rating: the average rating of all the books referencing this item
# sort: the sort value. For authors, this is the author_sort for that author
# category: the category (e.g., authors, series) that the item is in.
# Note that the "r'" in front of the { is necessary if there are backslashes
# (\ characters) in the template. It doesn't hurt anything to leave it there
# even if there aren't any backslashes.
categories_collapsed_name_template = r'{first.sort:shorten(4,,0)} - {last.sort:shorten(4,,0)}'
categories_collapsed_rating_template = r'{first.avg_rating:4.2f:ifempty(0)} - {last.avg_rating:4.2f:ifempty(0)}'
categories_collapsed_popularity_template = r'{first.count:d} - {last.count:d}'

#: Specify columns to sort the booklist by on startup
# Provide a set of columns to be sorted on when calibre starts.
# The argument is None if saved sort history is to be used
# otherwise it is a list of column,order pairs. Column is the
# lookup/search name, found using the tooltip for the column
# Order is 0 for ascending, 1 for descending.
# For example, set it to [('authors',0),('title',0)] to sort by
# title within authors.
sort_columns_at_startup = None

#: Control how dates are displayed
# Format to be used for publication date and the timestamp (date).
# A string controlling how the publication date is displayed in the GUI
# d the day as number without a leading zero (1 to 31)
# dd the day as number with a leading zero (01 to 31)
# ddd the abbreviated localized day name (e.g. 'Mon' to 'Sun')
# dddd the long localized day name (e.g. 'Monday' to 'Sunday')
# M the month as number without a leading zero (1-12)
# MM the month as number with a leading zero (01-12)
# MMM the abbreviated localized month name (e.g. 'Jan' to 'Dec')
# MMMM the long localized month name (e.g. 'January' to 'December')
# yy the year as two digit number (00-99)
# yyyy the year as four digit number
# h the hours without a leading 0 (0 to 11 or 0 to 23, depending on am/pm)
# hh the hours with a leading 0 (00 to 11 or 00 to 23, depending on am/pm)
# m the minutes without a leading 0 (0 to 59)
# mm the minutes with a leading 0 (00 to 59)
# s the seconds without a leading 0 (0 to 59)
# ss the seconds with a leading 0 (00 to 59)
# ap use a 12-hour clock instead of a 24-hour clock, with "ap" replaced by the lowercase localized string for am or pm
# AP use a 12-hour clock instead of a 24-hour clock, with "AP" replaced by the uppercase localized string for AM or PM
# aP use a 12-hour clock instead of a 24-hour clock, with "aP" replaced by the localized string for am or pm
# Ap use a 12-hour clock instead of a 24-hour clock, with "Ap" replaced by the localized string for AM or PM
# iso the date with time and timezone. Must be the only format present
# For example, given the date of 9 Jan 2010, the following formats show
# MMM yyyy ==> Jan 2010 yyyy ==> 2010 dd MMM yyyy ==> 09 Jan 2010
# MM/yyyy ==> 01/2010 d/M/yy ==> 9/1/10 yy ==> 10
#
# publication default if not set: MMM yyyy
# timestamp default if not set: dd MMM yyyy
# last_modified_display_format if not set: dd MMM yyyy gui_pubdate_display_format = 'MMM yyyy'
gui_timestamp_display_format = 'dd MMM yyyy'
gui_last_modified_display_format = 'dd MMM yyyy'

#: Control sorting of titles and series in the library display
# Control title and series sorting in the library view. If set to
# 'library_order', the title sort field will be used instead of the title.
# Unless you have manually edited the title sort field, leading articles such as
# The and A will be ignored. If set to 'strictly_alphabetic', the titles will be
# sorted as-is (sort by title instead of title sort). For example, with
# library_order, The Client will sort under 'C'. With strictly_alphabetic, the
# book will sort under 'T'.
# This flag affects calibre's library display. It has no effect on devices. In
# addition, titles for books added before changing the flag will retain their
# order until the title is edited. Editing a title and hitting Enter
# without changing anything is sufficient to change the sort. Or you can use
# the 'Update title sort' action in the Bulk metadata edit dialog to update
# it for many books at once.
title_series_sorting = 'library_order'

#: Control formatting of title and series when used in templates
# Control how title and series names are formatted when saving to disk/sending
# to device. The behavior depends on the field being processed. If processing
# title, then if this tweak is set to 'library_order', the title will be
# replaced with title_sort. If it is set to 'strictly_alphabetic', then the
# title will not be changed. If processing series, then if set to
# 'library_order', articles such as 'The' and 'An' will be moved to the end. If
# set to 'strictly_alphabetic', the series will be sent without change.
# For example, if the tweak is set to library_order, "The Lord of the Rings"
# will become "Lord of the Rings, The". If the tweak is set to
# strictly_alphabetic, it would remain "The Lord of the Rings". Note that the
# formatter function raw_field will return the base value for title and
# series regardless of the setting of this tweak.
save_template_title_series_sorting = 'library_order'

#: Set the list of words considered to be "articles" for sort strings
# Set the list of words that are to be considered 'articles' when computing the
# title sort strings. The articles differ by language. By default, calibre uses
# a combination of articles from English and whatever language the calibre user
# interface is set to. In addition, in some contexts where the book language is
# available, the language of the book is used. You can change the list of
# articles for a given language or add a new language by editing
# per_language_title_sort_articles. To tell calibre to use a language other
# than the user interface language, set, default_language_for_title_sort. For
# example, to use German, set it to 'deu'. A value of None means the user
# interface language is used. The setting title_sort_articles is ignored
# (present only for legacy reasons).
per_language_title_sort_articles = {
# English
'eng': (r'A\s+', r'The\s+', r'An\s+'),
# Esperanto
'epo': (r'La\s+', r"L'", 'L´'),
# Spanish
'spa': (r'El\s+', r'La\s+', r'Lo\s+', r'Los\s+', r'Las\s+', r'Un\s+', r'Una\s+', r'Unos\s+', r'Unas\s+'),
# French
'fra': (r'Le\s+', r'La\s+', r"L'", r'L´', r'L’', r'Les\s+', r'Un\s+', r'Une\s+', r'Des\s+', r'De\s+(La\s+)?', r"D'", r'D´', r'D’'),
# Polish
'pol': (),
# Italian
'ita': (r'Lo\s+', r'Il\s+', r"L'", r'L´', r'La\s+', r'Gli\s+', r'I\s+', r'Le\s+', r'Uno\s+', r'Un\s+', r'Una\s+', r"Un'", r'Un´', r'Dei\s+', r'Degli\s+', r'Delle\s+', r'Del\s+', r'Della\s+', r'Dello\s+', r"Dell'", r'Dell´'),
# Portuguese
'por': (r'A\s+', r'O\s+', r'Os\s+', r'As\s+', r'Um\s+', r'Uns\s+', r'Uma\s+', r'Umas\s+'),
# Romanian
'ron': (r'Un\s+', r'O\s+', r'Nişte\s+'),
# German
'deu': (r'Der\s+', r'Die\s+', r'Das\s+', r'Den\s+', r'Ein\s+', r'Eine\s+', r'Einen\s+', r'Dem\s+', r'Des\s+', r'Einem\s+', r'Eines\s+'),
# Dutch
'nld': (r'De\s+', r'Het\s+', r'Een\s+', r"'n\s+", r"'s\s+", r'Ene\s+', r'Ener\s+', r'Enes\s+', r'Den\s+', r'Der\s+', r'Des\s+', r"'t\s+"),
# Swedish
'swe': (r'En\s+', r'Ett\s+', r'Det\s+', r'Den\s+', r'De\s+'),
# Turkish
'tur': (r'Bir\s+',),
# Afrikaans
'afr': (r"'n\s+", r'Die\s+'),
# Greek
'ell': (r'O\s+', r'I\s+', r'To\s+', r'Ta\s+', r'Tus\s+', r'Tis\s+', r"'Enas\s+", r"'Mia\s+", r"'Ena\s+", r"'Enan\s+"),
# Hungarian
'hun': (r'A\s+', r'Az\s+', r'Egy\s+'),
}
default_language_for_title_sort = None title_sort_articles = r'^(A|The|An)\s+'

#: Specify a folder calibre should connect to at startup
# Specify a folder that calibre should connect to at startup using
# connect_to_folder. This must be a full path to the folder. If the folder does
# not exist when calibre starts, it is ignored.
# Example for Windows:
# auto_connect_to_folder = 'C:/Users/someone/Desktop/testlib'
# Example for other operating systems:
# auto_connect_to_folder = '/home/dropbox/My Dropbox/someone/library'
auto_connect_to_folder = ''

#: Specify renaming rules for SONY collections
# Specify renaming rules for SONY collections. This tweak is only applicable if
# metadata management is set to automatic. Collections on SONYs are named
# depending upon whether the field is standard or custom. A collection derived
# from a standard field is named for the value in that field.
#
# For example, if the standard 'series' column contains the value 'Darkover', then the
# collection name is 'Darkover'. A collection derived from a custom field will
# have the name of the field added to the value. For example, if a custom series
# column named 'My Series' contains the name 'Darkover', then the collection
# will by default be named 'Darkover (My Series)'. For purposes of this
# documentation, 'Darkover' is called the value and 'My Series' is called the
# category. If two books have fields that generate the same collection name,
# then both books will be in that collection.
#
# This set of tweaks lets you specify for a standard or custom field how
# the collections are to be named. You can use it to add a description to a
# standard field, for example 'Foo (Tag)' instead of the 'Foo'. You can also use
# it to force multiple fields to end up in the same collection.
#
# For example, you could force the values in 'series', '#my_series_1', and
# '#my_series_2' to appear in collections named 'some_value (Series)', thereby
# merging all of the fields into one set of collections.
#
# There are two related tweaks. The first determines the category name to use
# for a metadata field. The second is a template, used to determines how the
# value and category are combined to create the collection name.
# The syntax of the first tweak, sony_collection_renaming_rules, is:
# {'field_lookup_name':'category_name_to_use', 'lookup_name':'name', ...}
#
# The second tweak, sony_collection_name_template, is a template. It uses the
# same template language as plugboards and save templates. This tweak controls
# how the value and category are combined together to make the collection name.
# The only two fields available are {category} and {value}. The {value} field is
# never empty. The {category} field can be empty. The default is to put the
# value first, then the category enclosed in parentheses, it isn't empty:
# '{value} {category:|(|)}'
#
# Examples: The first three examples assume that the second tweak
# has not been changed.
#
# 1) I want three series columns to be merged into one set of collections. The
# column lookup names are 'series', '#series_1' and '#series_2'. I want nothing
# in the parenthesis. The value to use in the tweak value would be:
# sony_collection_renaming_rules={'series':'', '#series_1':'', '#series_2':''}
#
# 2) I want the word '(Series)' to appear on collections made from series, and
# the word '(Tag)' to appear on collections made from tags. Use:
# sony_collection_renaming_rules={'series':'Series', 'tags':'Tag'}
#
# 3) I want 'series' and '#myseries' to be merged, and for the collection name
# to have '(Series)' appended. The renaming rule is:
# sony_collection_renaming_rules={'series':'Series', '#myseries':'Series'}
#
# 4) Same as example 2, but instead of having the category name in parentheses
# and appended to the value, I want it prepended and separated by a colon, such
# as in Series: Darkover. I must change the template used to format the category name
#
# The resulting two tweaks are:
# sony_collection_renaming_rules={'series':'Series', 'tags':'Tag'}
# sony_collection_name_template='{category:||: }{value}'
sony_collection_renaming_rules = {}
sony_collection_name_template = '{value}{category:| (|)}'

#: Specify how SONY collections are sorted
# Specify how SONY collections are sorted. This tweak is only applicable if
# metadata management is set to automatic. You can indicate which metadata is to
# be used to sort on a collection-by-collection basis. The format of the tweak
# is a list of metadata fields from which collections are made, followed by the
# name of the metadata field containing the sort value.
# Example: The following indicates that collections built from pubdate and tags
# are to be sorted by the value in the custom column '#mydate', that collections
# built from 'series' are to be sorted by 'series_index', and that all other
# collections are to be sorted by title. If a collection metadata field is not
# named, then if it is a series- based collection it is sorted by series order,
# otherwise it is sorted by title order.
# [(['pubdate', 'tags'],'#mydate'), (['series'],'series_index'), (['*'], 'title')]
# Note that the bracketing and parentheses are required. The syntax is
# [ ( [list of fields], sort field ) , ( [ list of fields ] , sort field ) ]
# Default: empty (no rules), so no collection attributes are named.
sony_collection_sorting_rules = []

#: Control how tags are applied when copying books to another library
# Set this to True to ensure that tags in 'Tags to add when adding
# a book' are added when copying books to another library add_new_book_tags_when_importing_books = False

#: Set the maximum number of sort 'levels'
# Set the maximum number of sort 'levels' that calibre will use to resort the
# library after certain operations such as searches or device insertion. Each
# sort level adds a performance penalty. If the database is large (thousands of
# books) the penalty might be noticeable. If you are not concerned about multi-
# level sorts, and if you are seeing a slowdown, reduce the value of this tweak.
maximum_resort_levels = 5

#: Choose whether dates are sorted using visible fields
# Date values contain both a date and a time. When sorted, all the fields are
# used, regardless of what is displayed. Set this tweak to True to use only
# the fields that are being displayed.
sort_dates_using_visible_fields = False

#: Fuzz value for trimming covers
# The value used for the fuzz distance when trimming a cover.
# Colors within this distance are considered equal.
# The distance is in absolute intensity units.
cover_trim_fuzz_value = 10

#: Control behavior of the book list
# You can control the behavior of double clicks and pressing Enter on the books
# list. Choices: open_viewer, do_nothing, show_book_details,
# show_locked_book_details, edit_cell, edit_metadata. Selecting anything other
# than open_viewer, show_book_details, or show_locked_book_details has the side
# effect of disabling editing a field using a single click.
# Default: open_viewer.
# Example: doubleclick_on_library_view = 'do_nothing'
# You can also control whether the book list scrolls per item or
# per pixel. Default is per item.
doubleclick_on_library_view = 'open_viewer'
enter_key_behavior = 'do_nothing'
horizontal_scrolling_per_column = False vertical_scrolling_per_row = False

#: Language to use when sorting
# Setting this tweak will force sorting to use the
# collating order for the specified language. This might be useful if you run
# calibre in English but want sorting to work in the language where you live.
# Set the tweak to the desired ISO 639-1 language code, in lower case.
# You can find the list of supported locales at
# https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
# Default: locale_for_sorting = '' -- use the language calibre displays in
# Example: locale_for_sorting = 'fr' -- sort using French rules.
# Example: locale_for_sorting = 'nb' -- sort using Norwegian rules.
locale_for_sorting = ''

#: The number of seconds to wait before sending emails
# The number of seconds to wait before sending emails when using a
# public email server like GMX/Hotmail/Gmail. Default is: 5 minutes
# Setting it to lower may cause the server's SPAM controls to kick in,
# making email sending fail. Changes will take effect only after a restart of
# calibre. You can also change the list of hosts that calibre considers
# to be public relays here. Any relay host ending with one of the suffixes
# in the list below will be considered a public email server.
public_smtp_relay_delay = 301 public_smtp_relay_host_suffixes = ['gmail.com', 'live.com', 'gmx.com', 'outlook.com']

#: The maximum width and height for covers saved in the calibre library
# All covers in the calibre library will be resized, preserving aspect ratio,
# to fit within this size. This is to prevent slowdowns caused by extremely
# large covers maximum_cover_size = (1650, 2200)

#: Where to send downloaded news
# When automatically sending downloaded news to a connected device, calibre
# will by default send it to the main memory. By changing this tweak, you can
# control where it is sent. Valid values are "main", "carda", "cardb". Note
# that if there isn't enough free space available on the location you choose,
# the files will be sent to the location with the most free space.
send_news_to_device_location = 'main'

#: Unified toolbar on macOS
# If you enable this option and restart calibre, the toolbar will be 'unified'
# with the titlebar as is normal for macOS applications. However, doing this has
# various bugs, for instance the minimum width of the toolbar becomes twice
# what it should be and it causes other random bugs on some systems, so turn it
# on at your own risk!
unified_title_toolbar_on_osx = False

#: Save original file when converting/polishing from same format to same format
# When calibre does a conversion from the same format to the same format, for
# example, from EPUB to EPUB, the original file is saved, so that in case the
# conversion is poor, you can tweak the settings and run it again. By setting
# this to False you can prevent calibre from saving the original file.
# Similarly, by setting save_original_format_when_polishing to False you can
# prevent calibre from saving the original file when polishing.
save_original_format = True save_original_format_when_polishing = True

#: Number of recently viewed books to show
# Right-clicking the "View" button shows a list of recently viewed books. Control
# how many should be shown, here.
gui_view_history_size = 15

#: Change the font size of the Book details panel in the interface
# Change the font size at which book details are rendered in the side panel and
# comments are rendered in the metadata edit dialog. Set it to a positive or
# negative number to increase or decrease the font size. Similarly, change the
# font size of the widget used to converse with AI.
change_book_details_font_size_by = 0 change_ai_chat_font_size_by = 0

#: What format to default to when using the "Unpack book" feature
# The "Unpack book" feature of calibre allows direct editing of a book format.
# If multiple formats are available, calibre will offer you a choice
# of formats, defaulting to your preferred output format if it is available.
# Set this tweak to a specific value of 'EPUB' or 'AZW3' to always default
# to that format rather than your output format preference.
# Set to a value of 'remember' to use whichever format you chose last time you
# used the "Unpack book" feature.
# Examples:
# default_tweak_format = None (Use output format)
# default_tweak_format = 'EPUB'
# default_tweak_format = 'remember'
default_tweak_format = None

#: Do not preselect a completion when editing authors/tags/series/etc.
# This means that you can make changes and press Enter and your changes will
# not be overwritten by a matching completion. However, if you wish to use the
# completions you will now have to press Tab to select one before pressing
# Enter. Which technique you prefer will depend on the state of metadata in
# your library and your personal editing style.
#
# If preselect_first_completion is False and you want Tab to accept what you
# typed instead of the first completion then set tab_accepts_uncompleted_text
# to True. If you do this then to select from the completions you must press
# the Down or Up arrow keys. The tweak tab_accepts_uncompleted_text is ignored
# if preselect_first_completion is True preselect_first_completion = False tab_accepts_uncompleted_text = False

#: Completion mode when editing authors/tags/series/etc.
# By default, when completing items, calibre will show you all the candidates
# that start with the text you have already typed. You can instead have it show
# all candidates that contain the text you have already typed. To do this, set
# completion_mode to 'contains'. For example, if you type asi it will match both
# Asimov and Quasimodo, whereas the default behavior would match only Asimov.
# There is also a 'word-prefix' mode that matches only at the start of words,
# so typing 'asi' will match Asimov and "Isaac Asimov" but not Quasimodo.
# Similarly, typing 'cat' will match cathedral and "tabby cat" but not education.
#
# If completion_mode is 'word-prefix', you can specify additional word break chars
# with the extra_word_break_chars setting.
# For example, for extra_word_break_chars = '-' typing 'fic' will match
# both "Science Fiction" and "Science-Fiction", instead of only the first one.
completion_mode = 'prefix'
extra_word_break_chars = ''

#: Sort the list of libraries alphabetically
# The list of libraries in the Copy to library and Quick switch menus are
# normally sorted by most used. However, if there are more than a certain
# number of such libraries, the sorting becomes alphabetic. You can set that
# number here. The default is ten libraries.
many_libraries = 10

#: Choose available output formats for conversion
# Restrict the list of available output formats in the conversion dialogs.
# For example, if you only want to convert to EPUB and AZW3, change this to
# restrict_output_formats = ['EPUB', 'AZW3']. The default value of None causes
# all available output formats to be present.
restrict_output_formats = None

#: Set the thumbnail image quality used by the Content server
# The quality of a thumbnail is largely controlled by the compression quality
# used when creating it. Set this to a larger number to improve the quality.
# Note that the thumbnails get much larger with larger compression quality
# numbers.
# The value can be between 50 and 99 content_server_thumbnail_compression_quality = 75

#: Image file types to treat as e-books when dropping onto the "Book details" panel
# Normally, if you drop any image file in a format known to calibre onto the
# "Book details" panel, it will be used to set the cover. If you want to store
# some image types as e-books instead, you can set this tweak.
# Examples:
# cover_drop_exclude = {'tiff', 'webp'}
cover_drop_exclude = ()

#: Exclude fields when copy/pasting metadata
# You can ask calibre to not paste some metadata fields when using the
# Edit metadata->Copy metadata/Paste metadata actions. For example,
# exclude_fields_on_paste = ['cover', 'timestamp', '#mycolumn']
# to prevent pasting of the cover, Date and custom column, mycolumn.
# You can also add a shortcut in Preferences->Shortcuts->Edit metadata
# to paste metadata ignoring this tweak.
exclude_fields_on_paste = []

#: Skip internet connected check
# Skip checking whether the internet is available before downloading news.
# Useful if for some reason your operating systems network checking
# facilities are not reliable (for example NetworkManager on Linux).
skip_network_check = False

#: Tab stop width in the template editor
# Sets the width of the tab stop in the template editor in "average characters".
# For example, a value of 1 results in a space with the width of one average character.
template_editor_tab_stop_width = 4

#: Value for undefined numbers when sorting
# Sets the value to use for undefined numbers when sorting.
# For example, the value -10 sorts undefined numbers as if they were set to -10.
# Use 'maximum' for the largest possible number. Use 'minimum' for the smallest
# possible number. Quotes are optional if entering a number.
# Examples:
# value_for_undefined_numbers_when_sorting = -100
# value_for_undefined_numbers_when_sorting = '2'
# value_for_undefined_numbers_when_sorting = -0.01
# value_for_undefined_numbers_when_sorting = 'minimum'
# value_for_undefined_numbers_when_sorting = 'maximum'
value_for_undefined_numbers_when_sorting = 0

#: Allow template database functions in composite columns
# If True then the template database functions book_values() and book_count()
# can be used in composite custom columns. Note: setting this tweak to True and
# using these functions in composites can be very slow.
# Default: False allow_template_database_functions_in_composites = False

#: Change the programs that are run when opening files/URLs
# By default, calibre passes URLs to the operating system to open using
# whatever default programs are configured there. Here you can override
# that by specifying the program to use, per URL type. For local files,
# the type is "file" and for web links it is "http*". For example:
# openers_by_scheme = { "http*": "firefox %u" } will make calibre run Firefox
# for https://whatever URLs. %u is replaced by the URL to be opened. The scheme
# takes a glob pattern allowing a single entry to match multiple URL types.
openers_by_scheme = {}

#: Set the first day of the week for calendar popups
# It must be one of the values Default, Sunday, Monday, Tuesday, Wednesday,
# Thursday, Friday, or Saturday, all in English, spelled exactly as shown.
calendar_start_day_of_week = 'Default'

#: East Asian language to use for transliteration
# Setting this tweak will make calibre use the specified language as the "base"
# language when transliterating East Asian languages to English. This might be
# useful if you run calibre in English but want text transliterated to
# Japanese instead of Chinese. The valid values are:
# 'ja' for Japanese
# 'kr' for Korean
# 'vn' for Vietnamese
# 'zh' for Chinese
# Any other value will use the language set in calibre preferences as the base
# language. A base language other than those in the above list causes transliteration
# with a base language of Chinese.
# Example: east_asian_base_language = 'ja'
east_asian_base_language = ''

#: Hide AI features
# Hide AI features throughout the calibre user interface. Note that AI is
# already opt-in and no AI related code is even loaded until the user configures some
# AI backend. This tweak exists simply to hide a few menu entries and the like
# that mention AI.
hide_ai_features = False

#: Control GPU usage in Qt WebEngine
# Qt WebEngine is the library that is used to render content in the calibre
# viewer and editor. It can optionally use the GPU for enhanced performance,
# however, this can cause crashes/black screens on some older systems, so it
# is disabled by default. In normal usage, the performance difference is not
# noticeable anyway.
qt_webengine_uses_gpu = False

아이콘, 템플릿 등 재정의

참고

캘리버는 아이콘 테마를 직접 지원하며, 여러 아이콘 테마가 캘리버에 사용 가능합니다. :guilabel:`환경설정->인터페이스->모양 및 느낌->아이콘 테마 변경`으로 이동하여 사용할 수 있습니다. 개별 아이콘을 재정의하는 것보다 아이콘 테마를 사용하는 것이 바람직합니다.

캘리버는 아이콘, JavaScript, 메타데이터 재킷용 템플릿, 카탈로그 등의 정적 리소스를 원하는 사용자 정의 버전으로 재정의할 수 있습니다. 모든 정적 리소스는 캘리버 설치 위치의 resources 하위 폴더에 저장됩니다. Windows에서는 보통 C:\Program Files\Calibre2\app\resources`입니다. macOS에서는 :file:/Applications/calibre.app/Contents/Resources/resources/입니다. Linux에서 캘리버 웹사이트의 바이너리 설치 프로그램을 사용하는 경우 :file:/opt/calibre/resources`입니다. 이 경로는 캘리버를 설치하는 위치에 따라 달라질 수 있습니다.

이 resources 폴더의 파일을 변경하면 캘리버를 다음에 업데이트할 때 변경 사항이 덮어쓰여지므로 변경하지 않아야 합니다. 대신 :guilabel:`환경설정->고급->기타`로 이동하여 :guilabel:`캘리버 구성 폴더 열기`를 클릭하세요. 이 구성 폴더에 resources라는 하위 폴더를 만들고 재정의하려는 파일을 넣으세요. 파일을 적절한 하위 폴더에 배치하세요. 예를 들어 이미지는 :file:`resources/images`에 넣으세요. 캘리버는 다음에 시작할 때 내장 파일 대신 사용자 정의 파일을 자동으로 사용합니다.

예를 들어 책 제거 동작의 아이콘을 변경하려면 먼저 내장 resources 폴더에서 관련 파일이 :file:`resources/images/remove_books.png`임을 확인하세요. :file:`my_remove_books.png`라는 대체 아이콘이 PNG 형식으로 있으면 구성 폴더에 :file:`resources/images/remove_books.png`로 저장하세요. 캘리버 사용자 인터페이스에서 사용하는 모든 아이콘은 :file:`resources/images`와 그 하위 폴더에 있습니다. 여기에 재정의 파일을 배치하면 사용자 정의 아이콘 테마보다 더 높은 우선순위를 갖습니다.

캘리버용 사용자 정의 아이콘 테마 만들기

아름다운 아이콘 세트를 만들었고 캘리버의 내장 아이콘 테마 지원을 통해 다른 캘리버 사용자와 공유하고 싶다면 아이콘을 테마로 쉽게 패키징할 수 있습니다. 그렇게 하려면 환경설정 → 기타 → 아이콘 테마 만들기`로 이동하여 아이콘을 넣은 폴더를 선택하세요. 그런 다음 테마 메타데이터를 입력하고 확인을 클릭하세요. 이렇게 하면 테마 아이콘이 포함된 ZIP 파일이 생성됩니다. 이를 `Mobileread <https://www.mobileread.com/forums/forumdisplay.php?f=166>`__의 캘리버 포럼에 업로드하면 캘리버의 내장 아이콘 테마 시스템을 통해 테마를 사용할 수 있습니다. 기본적으로 방금 만든 아이콘 테마는 캘리버의 현재 테마로도 설치됩니다. 테마를 테스트하는 경우 테마의 아이콘이 사용되도록 :file:`resources/images 폴더에서 이미지를 제거하는 것을 잊지 마세요.

캘리버 6부터는 라이트 모드와 다크 모드에 대한 사용자 정의 아이콘을 가질 수 있습니다. 아이콘의 두 버전을 만들고 파일 이름에 -for-dark-theme``와 ``-for-light-theme 접미사를 붙이면 됩니다. 예를 들어 modified-for-dark-theme.png``와 ``modified-for-light-theme.png. 그러면 캘리버는 현재 테마에 따라 적절한 아이콘을 자동으로 사용합니다.

플러그인으로 캘리버 사용자 정의

캘리버는 매우 모듈화된 설계를 가지고 있습니다. 캘리버의 거의 모든 기능이 플러그인 형태로 제공됩니다. 플러그인은 변환, 뉴스 다운로드(이것은 레시피라고 합니다), 사용자 인터페이스의 다양한 구성 요소, 다양한 장치 연결, 캘리버에 파일 추가 시 파일 처리 등에 사용됩니다. :guilabel:`환경설정->고급->플러그인`으로 이동하여 캘리버의 모든 내장 플러그인의 전체 목록을 볼 수 있습니다.

캘리버의 동작을 사용자 정의하고 확장하기 위해 자신만의 플러그인을 작성할 수 있습니다. 캘리버의 플러그인 아키텍처는 매우 간단합니다. 직접 플러그인을 작성하여 calibre 기능 확장하기 튜토리얼을 참조하세요.

플러그인을 작성하고 나면 `Mobileread <https://www.mobileread.com/forums/forumdisplay.php?f=237>`__의 캘리버 플러그인 포럼에 업로드할 수 있으며 캘리버의 내장된 플러그인 업데이터를 통해 사용할 수 있습니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 캘리버 사용자 정의 »
- 플러그인용 API 문서

플러그인용 API 문서

강력한 플러그인을 만들기 위해 하위 클래스로 만들 수 있는 여러 추상 기본 클래스를 정의합니다. 유용한 클래스는 다음과 같습니다:

- Plugin

- FileTypePlugin

- 메타데이터 플러그인

- 카탈로그 플러그인

- 메타데이터 다운로드 플러그인

- 변환 플러그인

- 장치 드라이버

- 사용자 인터페이스 동작

- 환경설정 플러그인

Plugin
class calibre.customize.Plugin(plugin_path)[소스]

calibre 플러그인입니다. 유용한 멤버는 다음과 같습니다:

- self.installation_type: 플러그인이 설치된 방식을 저장합니다.

- self.plugin_path: 이 플러그인을 포함하는 ZIP 파일의 경로를

저장하며, 내장 플러그인인 경우에는 None입니다.

- self.site_customization: 사용자가 입력한 사용자 지정 문자열을

저장합니다.

하위 클래스에서 재정의해야 하는 메서드:

- initialize()

- customization_help()

유용한 메서드:

- 임시_파일()

- __enter__()

- load_resources()

supported_platforms = []

이 플러그인이 동작하는 플랫폼 목록입니다. 예: ['windows', 'osx', 'linux']

name = 'Trivial Plugin'

이 플러그인의 이름입니다. 작동하게 하려면 Trivial Plugin 이외의 값으로 설정해야 합니다.

version = (1, 0, 0)

이 플러그인의 버전입니다. 3-튜플(메이저, 마이너, 리비전) 형식입니다.

description = '아무것도 반응하지 않음'

이 플러그인이 하는 일을 설명하는 짧은 문자열입니다.

author = '알 수 없음'

이 플러그인의 작성자입니다.

priority = 1

하나의 파일 형식에 대해 둘 이상의 플러그인이 존재하면, 우선순위가 높은 것부터 순서대로 실행됩니다. 우선순위가 높은 플러그인이 먼저 실행됩니다. 가능한 가장 높은 우선순위는 sys.maxsize 입니다. 기본 우선순위는 1입니다.

minimum_calibre_version = (0, 4, 118)

이 플러그인이 필요로 하는 calibre의 최소 버전입니다.

installation_type = None

이 플러그인이 설치된 방식입니다.

can_be_disabled = True

False이면 사용자가 이 플러그인을 비활성화할 수 없습니다. 신중하게 사용하십시오.

type = '기초'

이 플러그인의 유형입니다. GUI에서 플러그인을 분류하는 데 사용됩니다.

initialize()[소스]

calibre 플러그인이 초기화될 때 한 번 호출됩니다. 새 플러그인이 추가될 때마다 플러그인은 다시 초기화됩니다. 또한 책 추가와 같은 작업자 프로세스에서 플러그인이 실행되는 경우, 새로운 작업자 프로세스마다 플러그인이 초기화된다는 점에 유의하십시오.

여기에서 플러그인별 초기화를 수행하십시오. 예를 들어 플러그인 ZIP 파일에서 리소스를 추출할 수 있습니다. ZIP 파일의 경로는 self.plugin_path 에서 사용할 수 있습니다.

이 시점에는 self.site_customization 을 사용할 수 없습니다.

config_widget()[소스]

단순한 문자열 기반 기본 사용자 지정에 의존하는 대신, 사용자 지정 설정 대화상자를 사용하려면 플러그인에서 이 메서드와 save_settings() 를 구현하십시오.

이 메서드를 구현하는 경우 QWidget를 반환해야 합니다. 위젯에는 인수를 받지 않는 선택적 validate() 메서드가 있을 수 있으며, 사용자가 확인을 클릭한 직후 호출됩니다. 메서드가 True를 반환하는 경우에만 변경 사항이 적용됩니다.

어떤 이유로 지금 이 시점에 설정을 수행할 수 없다면 두 개의 문자열로 이루어진 튜플 (message, details)을 반환하십시오. 이 값은 사용자에게 경고 대화상자로 표시되며, 작업은 중단됩니다.

save_settings(config_widget)[소스]

config_widget로 사용자가 지정한 설정을 저장합니다.

매개변수:

config_widget – config_widget() 가 반환한 위젯입니다.

do_user_config(parent=None)[소스]

이 메서드는 이 플러그인의 설정 대화상자를 표시합니다. 사용자가 확인을 클릭하면 True, 그렇지 않으면 False를 반환합니다. 변경 사항은 자동으로 적용됩니다.

load_resources(names)[소스]

이 플러그인이 ZIP 파일(사용자 추가 플러그인)로 제공되는 경우, 이 메서드를 사용해 ZIP 파일에서 리소스를 불러올 수 있습니다.

예를 들어 이미지를 불러오려면:

pixmap = QPixmap()
pixmap.loadFromData(self.load_resources(['images/icon.png'])['images/icon.png'])
icon = QIcon(pixmap)

매개변수:

names – / 를 구분자로 사용한 ZIP 파일 내부 리소스 경로 목록

반환:

{name: file_contents} 형태의 사전입니다. ZIP 파일에서 찾지 못한 이름은 사전에 포함되지 않습니다.

customization_help(gui=False)[소스]

이 플러그인을 사용자 지정하는 방법에 대한 도움말 문자열을 반환합니다. 기본적으로는 NotImplementedError 를 발생시키며, 이는 플러그인에 사용자 지정이 필요 없음을 의미합니다.

하위 클래스에서 이 메서드를 재구현하면, 사용자는 이 플러그인의 사용자 지정을 위한 문자열을 입력하라는 요청을 받습니다. 사용자 지정 문자열은 self.site_customization 에서 사용할 수 있습니다.

사이트 사용자 지정은 무엇이든 될 수 있습니다. 예를 들어 사용자의 컴퓨터에 있는 필요한 바이너리의 경로일 수 있습니다.

매개변수:

gui – True이면 HTML 도움말을 반환하고, 그렇지 않으면 일반 텍스트 도움말을 반환합니다.

temporary_file(suffix)[소스]

파일 시스템상의 임시 파일인 파일 유사 객체를 반환합니다. 이 파일은 닫은 뒤에도 계속 사용 가능하며, 인터프리터가 종료될 때만 제거됩니다. 생성된 임시 파일의 전체 경로에 접근하려면 반환된 객체의 name 멤버를 사용하십시오.

매개변수:

suffix – 임시 파일에 붙을 접미사입니다.

cli_main(args)[소스]

이 메서드는 플러그인 명령줄 인터페이스의 기본 진입점입니다. 사용자가 calibre-debug -r “Plugin Name” 을 실행할 때 호출됩니다. 전달된 모든 인수는 args 변수에 들어 있습니다.

FileTypePlugin
class calibre.customize.FileTypePlugin(plugin_path)[소스]

기반 클래스: Plugin

특정 파일 형식 집합과 연관된 플러그인입니다.

file_types = {}

이 플러그인을 실행해야 하는 파일 형식 집합입니다. 모든 파일 형식에 대해 실행하려면 ‘*’를 사용하십시오. 예: {'lit', 'mobi', 'prc'}

on_import = False

True이면 책이 데이터베이스에 추가될 때 이 플러그인이 실행됩니다.

on_postimport = False

True이면 책이 데이터베이스에 추가된 뒤 이 플러그인이 실행됩니다. 이 경우 플러그인의 postimport 및 postadd 메서드가 호출됩니다.

on_postconvert = False

True이면 책이 변환된 뒤 이 플러그인이 실행됩니다. 이 경우 플러그인의 postconvert 메서드가 호출됩니다.

on_postdelete = False

True이면 데이터베이스에서 책 파일이 삭제된 뒤 이 플러그인이 실행됩니다. 이 경우 플러그인의 postdelete 메서드가 호출됩니다.

on_preprocess = False

True이면 변환 직전에 이 플러그인이 실행됩니다.

on_postprocess = False

True이면 변환 출력 플러그인이 생성한 최종 파일에 대해 변환 후 이 플러그인이 실행됩니다.

type = '파일 유형'

이 플러그인의 유형입니다. GUI에서 플러그인을 분류하는 데 사용됩니다.

run(path_to_ebook)[소스]

플러그인을 실행합니다. 하위 클래스에서 구현해야 합니다. 전자책에 필요한 수정 작업을 수행한 뒤 수정된 전자책의 절대 경로를 반환해야 합니다. 수정이 필요 없다면 원본 전자책의 경로를 반환해야 합니다. 오류가 발생하면 Exception을 일으켜야 합니다. 기본 구현은 원본 전자책의 경로를 그대로 반환합니다. 파일 형식 플러그인이 실행되기 전 원본 파일의 경로는 self.original_path_to_file 에서 사용할 수 있습니다.

수정된 전자책 파일은 temporary_file() 메서드로 만들어야 합니다.

매개변수:

path_to_ebook – 전자책의 절대 경로입니다.

반환:

수정된 전자책의 절대 경로입니다.

postimport(book_id, book_format, db)[소스]

가져오기 후, 즉 책 파일이 데이터베이스에 추가된 뒤 호출됩니다. 이는 책 레코드가 처음 생성될 때 호출되는 postadd() 와는 다릅니다. 이 메서드는 책 레코드에 새 파일이 추가될 때마다 호출됩니다. 새로 추가된 파일의 내용을 바탕으로 책 레코드를 수정하는 데 유용합니다.

매개변수:

- book_id – 추가된 책의 데이터베이스 id입니다.

- book_format – 추가된 책의 파일 형식입니다.

- db – 라이브러리 데이터베이스입니다.

postconvert(book_id, book_format, db)[소스]

변환 후, 즉 변환 결과 책 파일이 데이터베이스에 추가된 뒤 호출됩니다. 책 추가 후가 아니라 변환 후에만 실행됩니다. 새로 추가된 파일의 내용을 바탕으로 책 레코드를 수정하는 데 유용합니다.

매개변수:

- book_id – 추가된 책의 데이터베이스 id입니다.

- book_format – 추가된 책의 파일 형식입니다.

- db – 라이브러리 데이터베이스입니다.

postdelete(book_id, book_format, db)[소스]

삭제 후, 즉 책 파일이 데이터베이스에서 삭제된 뒤 호출됩니다. 책 레코드가 삭제될 때는 실행되지 않고, 책에서 하나 이상의 형식이 삭제될 때만 실행됩니다. 삭제된 파일 형식을 바탕으로 책 레코드를 수정하는 데 유용합니다.

매개변수:

- book_id – 추가된 책의 데이터베이스 id입니다.

- book_format – 추가된 책의 파일 형식입니다.

- db – 라이브러리 데이터베이스입니다.

postadd(book_id, fmt_map, db)[소스]

추가 후, 즉 책이 DB에 추가된 뒤 호출됩니다. 이는 단일 책 파일이 책에 추가된 뒤 호출되는 postimport() 와는 다릅니다. postadd() 는 하나 이상의 책 파일을 포함할 수 있는 전체 책 레코드가 처음 생성될 때만 호출됩니다. 책이 calibre에 처음 추가될 때 데이터베이스의 책 레코드를 수정하고 싶다면 유용합니다.

매개변수:

- book_id – 추가된 책의 데이터베이스 id입니다.

- fmt_map – 파일 형식에서 추가된 경로로의 매핑입니다. 경우에 따라 파일이 스트림으로 추가되기도 하므로, 실제로 존재하는 파일을 가리킬 수도 있고 아닐 수도 있습니다. 이 경우 더미 값이나 존재하지 않는 경로일 수 있습니다.

- db – 라이브러리 데이터베이스

메타데이터 플러그인

class calibre.customize.MetadataReaderPlugin(*args, **kwargs)[소스]

기반 클래스: Plugin

여러 파일 형식에서 메타데이터를 읽는 기능을 구현하는 플러그인입니다.

file_types = {}

이 플러그인을 실행해야 하는 파일 형식 집합입니다. 예: set(['lit', 'mobi', 'prc'])

supported_platforms = ['windows', 'osx', 'linux']

이 플러그인이 동작하는 플랫폼 목록입니다. 예: ['windows', 'osx', 'linux']

version = (9, 7, 0)

이 플러그인의 버전입니다. 3-튜플(메이저, 마이너, 리비전) 형식입니다.

author = 'Kovid Goyal'

이 플러그인의 작성자입니다.

type = '메타데이터 읽기도구'

이 플러그인의 유형입니다. GUI에서 플러그인을 분류하는 데 사용됩니다.

get_metadata(stream, type)[소스]

stream(읽기를 지원하는 파일 유사 객체)으로 표현된 파일의 메타데이터를 반환합니다. 입력 데이터에 오류가 있으면 예외를 발생시킵니다.

매개변수:

type – 파일 형식입니다. file_types 의 항목 가운데 하나임이 보장됩니다.

반환:

calibre.ebooks.metadata.book.Metadata 객체입니다.

class calibre.customize.MetadataWriterPlugin(*args, **kwargs)[소스]

기반 클래스: Plugin

여러 파일 형식에서 메타데이터를 읽는 기능을 구현하는 플러그인입니다.

file_types = {}

이 플러그인을 실행해야 하는 파일 형식 집합입니다. 예: set(['lit', 'mobi', 'prc'])

supported_platforms = ['windows', 'osx', 'linux']

이 플러그인이 동작하는 플랫폼 목록입니다. 예: ['windows', 'osx', 'linux']

version = (9, 7, 0)

이 플러그인의 버전입니다. 3-튜플(메이저, 마이너, 리비전) 형식입니다.

author = 'Kovid Goyal'

이 플러그인의 작성자입니다.

type = '메타데이터 쓰기도구'

이 플러그인의 유형입니다. GUI에서 플러그인을 분류하는 데 사용됩니다.

set_metadata(stream, mi, type)[소스]

stream(읽기를 지원하는 파일 유사 객체)으로 표현된 파일의 메타데이터를 설정합니다. 입력 데이터에 오류가 있으면 예외를 발생시킵니다.

매개변수:

- type – 파일 형식입니다. file_types 의 항목 가운데 하나임이 보장됩니다.

- mi – calibre.ebooks.metadata.book.Metadata 객체입니다.

카탈로그 플러그인

class calibre.customize.CatalogPlugin(plugin_path)[소스]

기반 클래스: Plugin

범주 생성도구를 구현하는 플러그인입니다.

file_types = {}

이 플러그인이 실행되어야 하는 출력 파일 형식입니다. 예: ‘epub’ 또는 ‘xml’

type = '범주 생성도구'

이 플러그인의 유형입니다. GUI에서 플러그인을 분류하는 데 사용됩니다.

cli_options = []

이 플러그인 전용 CLI 파서 옵션으로, namedtuple Option 으로 선언합니다:

from collections import namedtuple Option = namedtuple(‘Option’, ‘option, default, dest, help’) cli_options = [Option(’–catalog-title’, default = ‘My Catalog’, dest = ‘catalog_title’, help = (_(‘Title of generated catalog. nDefault:’) + “ ‘” + ‘%default’ + “’”))] cli_options 는 calibre.db.cli.cmd_catalog:option_parser() 에서 파싱됩니다.

initialize()[소스]

플러그인이 내장 플러그인이 아니라면, 플러그인의 .ui 및 .py 파일을 ZIP 파일에서 $TMPDIR로 복사합니다. 탭은 동적으로 생성되어 calibre.gui2.dialogs.catalog.py:Catalog 의 카탈로그 옵션 대화상자에 추가됩니다.

run(path_to_output, opts, db, ids, notification=None)[소스]

플러그인을 실행합니다. 하위 클래스에서 구현해야 합니다. file_types 에 지정된 형식으로 카탈로그를 생성하고, 생성된 카탈로그 파일의 절대 경로를 반환해야 합니다. 오류가 발생하면 Exception을 일으켜야 합니다.

생성된 카탈로그 파일은 temporary_file() 메서드로 만들어야 합니다.

매개변수:

- path_to_output – 생성된 카탈로그 파일의 절대 경로입니다.

- opts – 키워드 인수 사전입니다.

- db – LibraryDatabase2 객체입니다.

메타데이터 다운로드 플러그인
class calibre.ebooks.metadata.sources.base.Source(*args, **kwargs)[소스]

기반 클래스: Plugin

type = '메타데이터 출처'

이 플러그인의 유형입니다. GUI에서 플러그인을 분류하는 데 사용됩니다.

author = 'Kovid Goyal'

이 플러그인의 작성자입니다.

supported_platforms = ['windows', 'osx', 'linux']

이 플러그인이 동작하는 플랫폼 목록입니다. 예: ['windows', 'osx', 'linux']

capabilities = frozenset({})

이 플러그인이 지원하는 기능 집합입니다. 유용한 기능에는 ‘identify’, ‘cover’ 가 있습니다.

touched_fields = frozenset({})

식별 단계 동안 이 플러그인이 잠재적으로 내려받을 수 있는 메타데이터 필드 목록입니다.

has_html_comments = False

플러그인이 HTML 형식의 설명을 반환한다면 True로 설정하십시오.

supports_gzip_transfer_encoding = False

이를 True로 설정하면 브라우저 객체가 gzip 전송 인코딩을 지원한다고 표시합니다. 이렇게 하면 다운로드가 빨라질 수 있지만, 먼저 소스가 gzip 전송 인코딩을 올바르게 지원하는지 확인하십시오.

ignore_ssl_errors = False

이 소스에 연결할 때 HTTPS 인증서 오류를 무시하려면 True로 설정하십시오.

cached_cover_url_is_reliable = True

캐시된 표지 URL은 때때로 신뢰할 수 없습니다(예: 다운로드가 실패하거나 반환된 이미지가 잘못되었을 수 있음). 이 소스에서 그런 경우가 자주 발생한다면 False로 설정하십시오.

options = ()

Option 객체 목록입니다. 이 플러그인의 설정 위젯을 자동으로 구성하는 데 사용됩니다.

config_help_message = None

이 플러그인의 설정 위젯 상단에 표시되는 문자열입니다.

can_get_multiple_covers = False

True이면 이 소스는 주어진 질의에 대해 여러 표지를 반환할 수 있습니다.

auto_trim_covers = False

True로 설정하면 이 플러그인으로 다운로드한 표지는 자동으로 다듬어집니다.

prefer_results_with_isbn = True

True로 설정되어 있고, 이 소스가 어떤 질의에 대해 여러 결과를 반환하는데 일부에는 ISBN이 있고 일부에는 ISBN이 없다면, ISBN이 없는 결과는 무시됩니다.

is_configured()[소스]

플러그인을 사용하기 전에 설정이 필요하다면 False를 반환하십시오. 예를 들어 사용자 이름/비밀번호/API 키가 필요할 수 있습니다.

customization_help()[소스]

이 플러그인을 사용자 지정하는 방법에 대한 도움말 문자열을 반환합니다. 기본적으로는 NotImplementedError 를 발생시키며, 이는 플러그인에 사용자 지정이 필요 없음을 의미합니다.

하위 클래스에서 이 메서드를 재구현하면, 사용자는 이 플러그인의 사용자 지정을 위한 문자열을 입력하라는 요청을 받습니다. 사용자 지정 문자열은 self.site_customization 에서 사용할 수 있습니다.

사이트 사용자 지정은 무엇이든 될 수 있습니다. 예를 들어 사용자의 컴퓨터에 있는 필요한 바이너리의 경로일 수 있습니다.

매개변수:

gui – True이면 HTML 도움말을 반환하고, 그렇지 않으면 일반 텍스트 도움말을 반환합니다.

config_widget()[소스]

단순한 문자열 기반 기본 사용자 지정에 의존하는 대신, 사용자 지정 설정 대화상자를 사용하려면 플러그인에서 이 메서드와 save_settings() 를 구현하십시오.

이 메서드를 구현하는 경우 QWidget를 반환해야 합니다. 위젯에는 인수를 받지 않는 선택적 validate() 메서드가 있을 수 있으며, 사용자가 확인을 클릭한 직후 호출됩니다. 메서드가 True를 반환하는 경우에만 변경 사항이 적용됩니다.

어떤 이유로 지금 이 시점에 설정을 수행할 수 없다면 두 개의 문자열로 이루어진 튜플 (message, details)을 반환하십시오. 이 값은 사용자에게 경고 대화상자로 표시되며, 작업은 중단됩니다.

save_settings(config_widget)[소스]

config_widget로 사용자가 지정한 설정을 저장합니다.

매개변수:

config_widget – config_widget() 가 반환한 위젯입니다.

get_author_tokens(authors, only_first_author=True)[소스]

저자 목록을 가져오고 AND 검색어에 유용한 토큰 목록을 반환합니다. 이 함수는 쉼표가 저자 이름에 있으면 이름이 성에 있고 다른 이름 형식이라고 가정하여 이름 중간 이름 성 순서로 토큰을 반환하려고 합니다.

get_title_tokens(title, strip_joiners=True, strip_subtitle=False)[소스]

제목을 가져와 AND 검색어에 유용한 토큰 목록을 반환합니다. 접속사(선택 사항) 및 구두점은 제외됩니다.

split_jobs(jobs, num)[소스]

작업 목록을 가능한 한 고르게, 최대 num 개의 그룹으로 나눕니다.

test_fields(mi)[소스]

mi 객체에서 null 인 self.touched_fields 의 첫 번째 필드를 반환합니다.

clean_downloaded_metadata(mi)[소스]

Metadata 객체를 result_queue 에 넣기 전에 메타데이터를 정규화하려면 플러그인의 identify 메서드에서 이 메서드를 호출하십시오. 물론 메타데이터 소스에 맞는 사용자 정의 알고리즘을 사용해도 됩니다.

get_book_url(identifiers)[소스]

3-튜플 또는 None을 반환합니다. 3-튜플의 형식은 (identifier_type, identifier_value, URL) 입니다. URL은 이 소스에서 identifiers 로 식별된 책의 URL입니다. identifier_type 과 identifier_value 는 URL에 해당하는 식별자를 지정합니다. 이 URL은 사람이 브라우저로 열어볼 수 있어야 합니다. 사용자가 이 소스의 책 페이지를 쉽게 방문할 수 있는 클릭 가능한 링크를 제공하기 위한 것입니다. URL을 찾지 못하면 None을 반환하십시오. 이 메서드는 빠르고 일관되어야 하므로, 식별자로부터 알려진 규칙에 따라 URL을 구성할 수 있을 때만 구현하십시오.

get_book_url_name(idtype, idval, url)[소스]

get_book_url() 의 반환값에서 사람이 읽기 쉬운 이름을 반환합니다.

get_book_urls(identifiers)[소스]

이 책에 대해 여러 URL을 반환하고 싶다면 이 메서드를 재정의하십시오. 3-튜플 목록을 반환하십시오. 기본적으로 이 메서드는 단순히 get_book_url() 을 호출합니다.

get_cached_cover_url(identifiers)[소스]

identifiers 사전으로 식별된 책의 캐시된 표지 URL을 반환합니다. 해당 URL이 없으면 None을 반환합니다.

이 메서드는 검증된 URL만 반환해야 한다는 점에 유의하십시오. 즉 일반 표지 이미지나 not found 오류를 일으킬 수 있는 URL은 반환해서는 안 됩니다.

id_from_url(url)[소스]

URL을 해석하여 (identifier_type, identifier_value) 형식의 튜플을 반환합니다. URL이 메타데이터 소스의 패턴과 맞지 않으면 None을 반환합니다.

identify_results_keygen(title=None, authors=None, identifiers={})[소스]

검색 쿼리(제목, 저자, 식별자)에 따라 메타데이터 객체를 관련성에 따라 정렬할 수 있는 키를 생성하는 데 사용되는 함수를 반환합니다.

이 키들은 identify() 호출 결과를 정렬하는 데 사용됩니다.

기본 알고리즘에 대한 자세한 내용은 InternalMetadataCompareKeyGen 을 참조하십시오. 기본 알고리즘이 적합하지 않다면 플러그인에서 이 함수를 재구현하십시오.

identify(log, result_queue, abort, title=None, authors=None, identifiers={}, timeout=30)[소스]

제목/저자/ISBN 등을 사용해 책을 식별합니다.

식별자(identifier)가 지정되었는데 일치 항목을 찾지 못했고, 이 메타데이터 소스가 관련 식별자를 모두 저장하지 않는 경우(예: 책의 모든 ISBN), 제목과 저자만으로 다시 시도해야 합니다(둘이 지정되어 있다면).

이 메타데이터 소스가 표지도 제공한다면, 동일한 ISBN/특수 식별자로 get covers API 를 다시 호출할 때 표지 URL을 다시 가져올 필요가 없도록 표지 URL을 캐시에 저장해야 합니다. 이를 위해 캐시 API 를 사용하십시오.

이 메서드가 result_queue 에 넣는 모든 Metadata 객체에는 source_relevance 속성이 있어야 하며, 이는 이 질의에 대해 메타데이터 소스가 결과를 반환한 순서를 나타내는 정수입니다. 이 정수는 compare_identify_results() 에서 사용됩니다. 순서가 중요하지 않다면 모든 결과에 대해 0으로 설정하십시오.

Metadata 객체를 result_queue 에 넣기 전에 표지/ISBN 매핑 정보가 모두 캐시되었는지 확인하십시오.

매개변수:

- log – 로그 객체입니다. 디버깅 정보/오류를 출력하는 데 사용하십시오.

- result_queue – 결과 Queue 입니다. 결과를 여기에 넣어야 합니다. 각 결과는 Metadata 객체입니다.

- abort – abort.is_set() 가 True를 반환하면 추가 처리를 중단하고 가능한 한 빨리 반환하십시오.

- title – 책 제목이며, None일 수 있습니다.

- authors – 책 저자 목록이며, None일 수 있습니다.

- identifiers – 기타 식별자 사전으로, 가장 흔한 예는 {‘isbn’:’1234…’} 입니다.

- timeout – 초 단위 시간 제한입니다. 어떤 네트워크 요청도 timeout 보다 오래 멈춰 있어서는 안 됩니다.

반환:

오류가 없으면 None, 그렇지 않으면 사용자에게 표시하기에 적합한 오류의 유니코드 표현입니다.

download_cover(log, result_queue, abort, title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False)[소스]

표지를 다운로드하여 result_queue 에 넣습니다. 매개변수의 의미는 모두 identify() 와 같습니다. (self, cover_data) 를 result_queue 에 넣으십시오.

가능하면 효율을 위해 이 메서드는 캐시된 표지 URL을 사용해야 합니다. 캐시 데이터가 없을 때는 대부분의 플러그인이 단순히 identify 를 호출하고 그 결과를 사용합니다.

get_best_cover 매개변수가 True이고 이 플러그인이 여러 표지를 가져올 수 있다면, “가장 좋은” 표지만 가져와야 합니다.

class calibre.ebooks.metadata.sources.base.InternalMetadataCompareKeyGen(mi, source_plugin, title, authors, identifiers)[소스]

주어진 검색어에서 메타데이터 객체의 관련성을 비교하기 위한 정렬 키를 생성합니다. 이것은 다른 소스가 아닌 동일한 메타데이터 소스의 결과를 비교하는 데만 사용됩니다.

정렬 키는 오름차순 정렬이 관련성 내림차순 정렬이 되도록 보장합니다.

알고리즘은 다음과 같습니다:

- 질의와 동일한 식별자가 하나 이상 있는 결과를 우선합니다.

- 캐시된 표지 URL이 있는 결과를 우선합니다.

- 사용 가능한 필드가 모두 채워진 결과를 우선합니다.

- 현재 사용자 인터페이스 언어와 같은 언어를 가진 결과를 우선합니다.

- 질의와 제목이 정확히 일치하는 결과를 우선합니다.

- 설명이 더 긴 결과를 우선합니다(10% 이상 더 긴 경우).

- 메타데이터 소스의 검색이 보고한 결과의 관련성을 사용합니다.

엔진

변환 플러그인
class calibre.customize.conversion.InputFormatPlugin(*args)[소스]

기반 클래스: Plugin

InputFormatPlugins 는 문서를 HTML+OPF+CSS+기타 형식으로 변환하는 역할을 합니다. 변환 결과는 반드시 UTF-8 로 인코딩되어야 합니다. 주요 동작은 convert() 에서 이루어집니다.

type = '변환 입력'

이 플러그인의 유형입니다. GUI에서 플러그인을 분류하는 데 사용됩니다.

supported_platforms = ['windows', 'osx', 'linux']

이 플러그인이 동작하는 플랫폼 목록입니다. 예: ['windows', 'osx', 'linux']

file_types = {}

이 플러그인을 실행해야 하는 파일 형식 집합입니다. 예: set(['azw', 'mobi', 'prc'])

is_image_collection = False

True이면 이 입력 플러그인은 HTML 파일마다 하나씩 이미지를 모은 컬렉션을 생성합니다. 입력 파일이 이미지 컬렉션일 수도 있고 아닐 수도 있다면 convert 메서드에서 동적으로 설정할 수 있습니다. 이를 True로 설정했다면 이미지 목록을 반환하는 get_images() 메서드를 구현해야 합니다.

core_usage = 1

이 플러그인이 사용하는 CPU 코어 수입니다. 값이 -1이면 사용 가능한 모든 코어를 사용합니다.

for_viewer = False

True로 설정하면 입력 플러그인이 출력 결과를 보기용으로 적합하게 만들기 위해 특별 처리를 수행합니다.

output_encoding = 'utf-8'

이 입력 플러그인이 파일을 생성할 때 사용하는 인코딩입니다. None이면 인코딩이 정의되지 않았으므로 각각 개별적으로 감지해야 합니다.

common_options = {<calibre.customize.conversion.OptionRecommendation object>}

모든 입력 형식 플러그인이 공유하는 옵션입니다. 하위 클래스에서 재정의하지 마십시오. 대신 options 를 사용하십시오. 모든 옵션은 OptionRecommendation 의 인스턴스여야 합니다.

options = {}

이 플러그인의 동작을 사용자 지정하는 옵션입니다. 모든 옵션은 OptionRecommendation 의 인스턴스여야 합니다.

recommendations = {}

(option_name, recommended_value, recommendation_level) 형식의 3-튜플 집합입니다.

get_images()[소스]

이 입력 플러그인이 이미지 컬렉션을 나타내는 경우, 이미지의 절대 경로 목록을 반환합니다. 이미지 목록의 순서는 spine 및 TOC 와 동일합니다.

convert(stream, options, file_ext, log, accelerators)[소스]

이 메서드는 하위 클래스에서 구현해야 합니다. 생성된 OPF 파일의 경로나 OEBBook 인스턴스를 반환해야 합니다. 모든 출력은 현재 폴더 안에 있어야 합니다. 이 플러그인이 현재 폴더 밖에 파일을 만든다면, 이 메서드가 반환되기 전에 해당 파일을 삭제하거나 삭제 대상으로 표시해야 합니다.

매개변수:

- stream – 입력 파일을 담고 있는 파일 유사 객체입니다.

- options – 변환 과정을 사용자 지정하는 옵션입니다. 이 플러그인이 선언한 모든 옵션에 대응하는 속성을 가진다는 것이 보장됩니다. 또한 0 이상의 정수 값을 가지는 verbose 속성도 있습니다. 숫자가 클수록 더 자세한 출력을 의미합니다. 또 하나 유용한 속성은 calibre.customize.profiles.InputProfile 의 인스턴스인 input_profile 입니다.

- file_ext – 입력 파일의 확장자(. 제외)입니다. 이 플러그인이 지원하는 file_types 중 하나임이 보장됩니다.

- log – calibre.utils.logging.Log 객체입니다. 모든 출력은 이 객체를 사용해야 합니다.

- accelerators – 입력 플러그인이 쉽게 얻을 수 있어 변환의 다음 단계들을 빠르게 할 수 있는 여러 정보의 사전입니다.

postprocess_book(oeb, opts, log)[소스]

입력 플러그인이 책이 파싱된 뒤 후처리를 수행할 수 있도록 호출됩니다.

specialize(oeb, opts, log, output_fmt)[소스]

특정 출력 형식에 맞게 파싱된 책을 입력 플러그인이 특화할 수 있도록 호출됩니다. postprocess_book 이후, 파싱된 책에 어떤 변환도 수행되기 전에 호출됩니다.

gui_configuration_widget(parent, get_option_by_name, get_option_help, db, book_id=None)[소스]

calibre GUI 에서 이 플러그인을 설정하는 데 사용할 위젯을 만들기 위해 호출됩니다. 위젯은 PluginWidget 클래스의 인스턴스여야 합니다. 예시는 내장 입력 플러그인을 참조하십시오.

class calibre.customize.conversion.OutputFormatPlugin(*args)[소스]

기반 클래스: Plugin

OutputFormatPlugins 는 OEB 문서(OPF+HTML)를 출력 전자책으로 변환하는 역할을 합니다.

OEB 문서는 UTF-8 로 인코딩되어 있다고 가정할 수 있습니다. 주요 동작은 convert() 에서 이루어집니다.

type = '변환 출력'

이 플러그인의 유형입니다. GUI에서 플러그인을 분류하는 데 사용됩니다.

supported_platforms = ['windows', 'osx', 'linux']

이 플러그인이 동작하는 플랫폼 목록입니다. 예: ['windows', 'osx', 'linux']

file_type = None

이 플러그인이 출력하는 파일 형식(앞에 마침표가 없는 확장자)입니다.

common_options = {<calibre.customize.conversion.OptionRecommendation object>}

모든 입력 형식 플러그인이 공유하는 옵션입니다. 하위 클래스에서 재정의하지 마십시오. 대신 options 를 사용하십시오. 모든 옵션은 OptionRecommendation 의 인스턴스여야 합니다.

options = {}

이 플러그인의 동작을 사용자 지정하는 옵션입니다. 모든 옵션은 OptionRecommendation 의 인스턴스여야 합니다.

recommendations = {}

(option_name, recommended_value, recommendation_level) 형식의 3-튜플 집합입니다.

property description

str(object=’’) -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

주어진 객체로부터 새 문자열 객체를 만듭니다. encoding 또는 errors 가 지정된 경우, 객체는 지정한 인코딩과 오류 처리기로 디코드할 수 있는 데이터 버퍼를 제공해야 합니다. 그렇지 않으면 object.__str__() 의 결과(정의되어 있다면) 또는 repr(object) 를 반환합니다. encoding 의 기본값은 ‘utf-8’ 이고, errors 의 기본값은 ‘strict’ 입니다.

convert(oeb_book, output, input_plugin, opts, log)[소스]

oeb_book`( :class:`calibre.ebooks.oeb.OEBBook 인스턴스)의 내용을 output 으로 지정한 파일에 렌더링합니다.

매개변수:

- output – 파일 유사 객체이거나 문자열입니다. 문자열이면 존재할 수도 있고 아닐 수도 있는 폴더의 경로입니다. 출력 플러그인은 그 폴더에 결과를 써야 합니다. 파일 유사 객체라면 출력 플러그인은 그 파일에 결과를 써야 합니다.

- input_plugin – 변환 파이프라인의 시작에서 사용된 입력 플러그인입니다.

- opts – 변환 옵션입니다. 이 플러그인의 OptionRecommendations 에 대응하는 속성을 가진다는 것이 보장됩니다.

- log – 로거입니다. 디버그/정보 메시지 등을 여기에 출력하십시오.

specialize_options(log, opts, input_fmt)[소스]

변환 파이프라인이 사용하는 변환 옵션 값을 바꾸는 데 사용할 수 있습니다.

specialize_css_for_output(log, opts, item, stylizer)[소스]

CSS 평탄화 과정에서 CSS 를 변경하는 데 사용할 수 있습니다.

매개변수:

- item – 처리 중인 항목(HTML 파일)

- stylizer – item 의 평탄화된 스타일을 담고 있는 Stylizer 객체입니다. stylizer.style(element) 로 임의의 요소에 대한 스타일을 얻을 수 있습니다.

gui_configuration_widget(parent, get_option_by_name, get_option_help, db, book_id=None)[소스]

calibre GUI 에서 이 플러그인을 설정하는 데 사용할 위젯을 만들기 위해 호출됩니다. 위젯은 PluginWidget 클래스의 인스턴스여야 합니다. 예시는 내장 출력 플러그인을 참조하십시오.

장치 드라이버

모든 장치 드라이버의 기본 클래스는 DevicePlugin 입니다. 그러나 장치가 운영 체제에 USBMS 드라이브로 노출된다면, 이러한 종류의 장치를 지원하는 데 필요한 모든 논리를 구현한 USBMS 클래스를 대신 사용해야 합니다.

class calibre.devices.interface.DevicePlugin(plugin_path)[소스]

기반 클래스: Plugin

전자책 리더기와 통신하는 백엔드가 구현해야 할 인터페이스를 정의합니다.

type = '장치 인터페이스'

이 플러그인의 유형입니다. GUI에서 플러그인을 분류하는 데 사용됩니다.

FORMATS = ['lrf', 'rtf', 'pdf', 'txt']

지원하는 형식의 정렬된 목록

VENDOR_ID = 0

VENDOR_ID 는 정수, 정수 목록 또는 사전이 될 수 있습니다. 사전인 경우에는 다음 형식의 사전들의 사전이어야 합니다:

{
integer_vendor_id : { product_id : [list of BCDs], ... },
...
}

PRODUCT_ID = 0

정수 또는 정수 목록

BCD = None

BCD 는 BCD를 기준으로 장치를 구분하지 않으려면 None 일 수 있고, 이 드라이버가 지원하는 모든 장치의 BCD 번호 목록일 수도 있습니다.

THUMBNAIL_HEIGHT = 68

장치의 썸네일 높이

THUMBNAIL_COMPRESSION_QUALITY = 75

썸네일의 압축 품질입니다. 100에 가깝게 설정할수록 압축 아티팩트가 줄어 더 나은 품질의 썸네일을 얻을 수 있습니다. 물론 썸네일 크기도 커집니다.

WANTS_UPDATED_THUMBNAILS = False

sync_booklists 중 표지 썸네일 업데이트를 장치가 지원한다면 True로 설정하십시오. True로 설정하면 book matching 중 device.py 가 표지 썸네일을 새로 고치도록 요청합니다.

CAN_SET_METADATA = ['title', 'authors', 'collections']

책의 메타데이터를 GUI를 통해 설정할 수 있는지 여부입니다.

CAN_DO_DEVICE_DB_PLUGBOARD = False

장치가 device_db 메타데이터 plugboard 를 처리할 수 있는지 여부입니다.

path_sep = '/'

장치의 책 경로에 사용하는 경로 구분자입니다.

icon = 'reader.png'

이 장치의 아이콘입니다.

UserAnnotation

:py:class:`~calibre.devices.interface.Annotation`의 별칭

OPEN_FEEDBACK_MESSAGE = None

None이 아니면 GUI가 상태 표시줄에 메시지로 표시합니다. 여는 데 시간이 오래 걸릴 때 유용합니다.

VIRTUAL_BOOK_EXTENSIONS = frozenset({})

장치에서 “가상 책” 으로 취급되어 보기/저장/라이브러리 추가가 불가능한 확장자 집합입니다. 예: frozenset(['kobo'])

VIRTUAL_BOOK_EXTENSION_MESSAGE = None

가상 책 확장자에 대해 사용자에게 표시할 메시지입니다.

NUKE_COMMENTS = None

장치로 보낸 책 사본의 설명을 제거할지 여부입니다. None이 아니라면, 설명을 대체할 짧은 문자열이어야 합니다.

MANAGES_DEVICE_PRESENCE = False

True이면 이 드라이버가 장치 감지, 꺼내기 등을 완전히 관리한다는 뜻입니다. 이를 True로 설정했다면 detect_managed_devices 와 debug_managed_device_detection 메서드를 반드시 구현해야 합니다. 이 값이 true 인 드라이버는 장치 감지, 장치 블랙리스트, 꺼낸 장치 목록 등을 관리할 책임이 있습니다. calibre 는 주기적으로 detect_managed_devices() 메서드를 호출하며, 감지된 장치를 반환하면 open() 을 호출합니다. 이전 open() 호출이 실패했더라도 장치가 반환될 때마다 open() 이 호출되므로, 드라이버는 실패한 장치의 자체 블랙리스트를 유지해야 합니다. 마찬가지로 꺼낼 때 calibre 는 eject() 를 호출하고, 다음 detect_managed_devices() 호출이 None을 반환한다고 가정하면 post_yank_cleanup() 을 호출합니다.

SLOW_DRIVEINFO = False

True로 설정하면 calibre 는 책 목록이 로드된 뒤 driveinfo 를 얻기 위해 get_driveinfo() 메서드를 호출합니다.

ASK_TO_ALLOW_CONNECT = False

True로 설정하면, 장치가 처음 감지되었을 때 calibre 가 사용자가 calibre 로 이 장치를 관리할지 물어봅니다. 이를 True로 설정했다면 get_device_uid(), ignore_connected_device(), get_user_blacklisted_devices(), set_user_blacklisted_devices() 를 구현해야 합니다.

user_feedback_after_callback = None

어떤 콜백이 실행된 뒤(현재는 upload_books 만) calibre 가 사용자에게 팝업 메시지를 띄우게 하려면 {‘title’:title, ‘msg’:msg, ‘det_msg’:detailed_msg} 형식의 사전으로 설정하십시오. 사용자에게 너무 많은 메시지를 남발하지 않도록 주의하십시오. 이 변수는 모든 콜백 뒤에 검사되므로, 정말 필요할 때만 설정하십시오.

classmethod get_open_popup_message()[소스]

GUI가 비모달 팝업으로 표시합니다. OpenPopupMessage 의 인스턴스여야 합니다.

classmethod model_metadata() → tuple[ModelMetadata, ...][소스]

이 드라이버가 지원하는 실제 장치 모델 전체에 대한 메타데이터입니다.

is_usb_connected(devices_on_system, debug=False, only_presence=False)[소스]

현재 이 플러그인이 처리하는 장치가 연결되어 있다면 True, device_info 를 반환합니다.

매개변수:

devices_on_system – 현재 연결된 장치 목록

detect_managed_devices(devices_on_system, force_refresh=False)[소스]

MANAGES_DEVICE_PRESENCE 가 True일 때만 호출됩니다.

이 드라이버가 처리할 수 있는 장치를 검색합니다. 장치를 찾으면 장치 객체를 반환해야 합니다. 이 객체는 connected_device 로 open() 메서드에 전달됩니다. 장치를 찾지 못하면 None을 반환하십시오. 반환되는 객체는 무엇이든 될 수 있으며 calibre 는 이를 사용하지 않고 open() 에만 전달합니다.

이 메서드는 GUI에서 주기적으로 호출되므로 너무 많은 리소스를 사용하지 않도록 하십시오. 반복적인 시스템 검색을 피하려면 캐시를 사용하십시오.

매개변수:

- devices_on_system – 시스템에서 발견된 USB 장치 집합입니다.

- force_refresh – 드라이버가 반복 검색 방지를 위해 캐시를 사용한다면, True일 때 캐시를 비워야 합니다.

debug_managed_device_detection(devices_on_system, output)[소스]

MANAGES_DEVICE_PRESENCE 가 True일 때만 호출됩니다.

시스템에서 감지된 장치에 대한 정보를 output(파일 유사 객체)에 기록해야 합니다.

장치가 감지되어 성공적으로 열렸다면 True, 그렇지 않으면 False를 반환해야 합니다.

reset(key='-1', log_packets=False, report_progress=None, detected_device=None)[소스]

매개변수:

- key – 장치 잠금을 해제하기 위한 키

- log_packets – True이면 장치로 오가는 패킷 스트림이 기록됩니다.

- report_progress – 여러 작업에 대해 % 진행률(0에서 100 사이의 숫자)을 받아 호출되는 함수입니다. -1로 호출되면 해당 작업에는 진행률 정보가 없다는 뜻입니다.

- detected_device – 장치 스캐너에서 가져온 장치 정보입니다.

can_handle_windows(usbdevice, debug=False)[소스]

이 드라이버가 해당 장치를 처리할 수 있는지 추가 검사하기 위한 선택적 메서드입니다. 처리할 수 없다면 False를 반환해야 합니다. 이 메서드는 vendor, product id, bcd 가 일치한 뒤에만 호출되므로 비교적 시간이 드는 검사를 수행할 수 있습니다. 기본 구현은 True를 반환합니다. 이 메서드는 Windows 에서만 호출됩니다. can_handle() 도 참조하십시오.

USBMS 기반 장치의 경우 이 메서드는 기본적으로 can_handle() 에 위임된다는 점에 유의하십시오. 따라서 USBMS 하위 클래스에서는 can_handle() 만 재정의하면 됩니다.

매개변수:

usbdevice – calibre.devices.winusb.scan_usb_devices() 가 반환한 usbdevice 입니다.

can_handle(device_info, debug=False)[소스]

can_handle_windows() 의 Unix 버전입니다.

매개변수:

device_info – (vid, pid, bcd, 제조사, 제품명, 일련 번호) 튜플입니다.

open(connected_device, library_uuid)[소스]

장치별 초기화를 수행합니다. 장치가 감지된 뒤이지만 장치와 통신하는 다른 함수보다 먼저 호출됩니다. 예를 들어 장치가 USB 대용량 저장소로 자신을 나타내는 경우, 이 메서드는 장치를 마운트하거나 자동 마운트된 경우 어디에 마운트되었는지 찾는 역할을 합니다. calibre.devices.usbms.device.Device.open() 메서드에는 USB 대용량 저장 장치에 대한 좋은 예시가 되는 구현이 있습니다.

이 메서드는 사용자에게 메시지를 표시하기 위해 OpenFeedback 예외를 발생시킬 수 있습니다.

매개변수:

- connected_device – 열려고 하는 장치입니다. (vendor id, product id, bcd, 제조사 이름, 제품명, 장치 일련 번호) 튜플입니다. 하지만 일부 장치에는 일련 번호가 없고, Windows 에서는 처음 세 필드만 있으며 나머지는 None 입니다.

- library_uuid – 현재 calibre 라이브러리의 UUID 입니다. 라이브러리가 없으면(예: 명령줄에서 사용할 때) None 일 수 있습니다.

eject()[소스]

OS에서 장치를 마운트 해제 / 꺼냅니다. 장치와 통신해야 하는 대기 중 GUI 작업이 있는지는 확인하지 않습니다.

참고: 이 메서드는 다른 장치 메서드들과 동일한 스레드에서 호출되지 않을 수 있습니다.

post_yank_cleanup()[소스]

사용자가 먼저 꺼내지 않고 장치를 갑자기 분리했을 때 호출됩니다.

set_progress_reporter(report_progress)[소스]

진행률 정보를 보고할 함수를 설정합니다.

매개변수:

report_progress – 여러 작업에 대해 % 진행률(0에서 100 사이의 숫자)을 받아 호출되는 함수입니다. -1로 호출되면 해당 작업에는 진행률 정보가 없다는 뜻입니다.

get_device_information(end_session=True)[소스]

장치에 장치 정보를 요청합니다. L{DeviceInfoQuery} 를 참조하십시오.

반환:

(장치 이름, 장치 버전, 장치의 소프트웨어 버전, MIME 형식) 튜플입니다. 선택적으로 다섯 번째 요소로 드라이브 정보 사전을 가질 수 있습니다. 예시는 usbms.driver 를 참조하십시오.

get_driveinfo()[소스]

driveinfo 사전을 반환합니다. 보통 get_device_information() 에서 호출되지만, 이 드라이버에서 driveinfo 로딩이 느리다면 SLOW_DRIVEINFO 를 설정해야 합니다. 이 경우 이 메서드는 책 목록이 로드된 뒤 calibre 가 호출합니다. 이 메서드는 장치 스레드에서 호출되지 않으므로, 드라이버는 books() 메서드에서 드라이브 정보를 캐시하고 이 함수는 캐시된 데이터를 반환해야 합니다.

card_prefix(end_session=True)[소스]

카드 경로 접두사의 2요소 목록을 반환합니다. 카드가 없으면 해당 카드의 접두사는 None 입니다. 예: (‘/place’, ‘/place2’) (None, ‘place2’) (‘place’, None) (None, None)

total_space(end_session=True)[소스]

마운트 지점의 총 사용 가능 공간을 가져옵니다:

1. 주 메모리

2. 메모리 카드 A

3. 메모리 카드 B

반환:

(1, 2, 3) 위치의 총 공간(바이트)으로 이루어진 3요소 목록입니다. 특정 장치에 이 위치가 없으면 0을 반환해야 합니다.

free_space(end_session=True)[소스]

마운트 지점의 남은 공간을 가져옵니다:

1. 주 메모리

2. 카드 A

3. 카드 B

반환:

(1, 2, 3) 위치의 남은 공간(바이트)으로 이루어진 3요소 목록입니다. 특정 장치에 이 위치가 없으면 -1을 반환해야 합니다.

books(oncard=None, end_session=True)[소스]

장치에 있는 전자책 목록을 반환합니다.

매개변수:

oncard – ‘carda’ 또는 ‘cardb’ 이면 해당 저장 카드의 전자책 목록을 반환하고, 그렇지 않으면 장치의 주 메모리에 있는 전자책 목록을 반환합니다. 카드를 지정했는데 카드에 책이 없으면 빈 목록을 반환합니다.

반환:

BookList 입니다.

upload_books(files, names, on_card=None, end_session=True, metadata=None)[소스]

책 목록을 장치에 업로드합니다. 장치에 이미 파일이 있다면 교체해야 합니다. 장치에 여유 공간이 충분하지 않으면 이 메서드는 FreeSpaceError 를 발생시켜야 합니다. FreeSpaceError 의 텍스트에는 on_card 가 None 이 아니면 반드시 “card” 라는 단어가, 그렇지 않으면 반드시 “memory” 라는 단어가 포함되어야 합니다.

매개변수:

- files – 경로 목록

- names – 장치에 업로드된 뒤 책이 가지게 될 파일명 목록입니다. len(names) == len(files)

- metadata – None이 아니면 Metadata 객체 목록입니다. 아이디어는 이 메타데이터를 사용해 장치의 어느 위치에 책을 둘지 결정하는 것입니다. len(metadata) == len(files). 일반 표지(표지 경로) 외에 thumbnail 속성이 있을 수도 있으며, 있을 경우 이를 우선 사용해야 합니다. thumbnail 속성의 형식은 (너비, 높이, jpeg 형식의 cover_data) 입니다.

반환:

3요소 튜플 목록입니다. 이 목록은 add_books_to_metadata() 에 전달하기 위한 것입니다.

classmethod add_books_to_metadata(locations, metadata, booklists)[소스]

booklist 에 위치를 추가합니다. 이 함수는 장치와 통신해서는 안 됩니다.

매개변수:

- locations – L{upload_books} 호출 결과

- metadata – upload_books() 와 동일한 Metadata 객체 목록입니다.

- booklists – (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=’cardb’)`) 호출 결과를 담은 튜플입니다.

delete_books(paths, end_session=True)[소스]

장치 경로에 있는 책을 삭제합니다.

classmethod remove_books_from_metadata(paths, booklists)[소스]

메타데이터 목록에서 책을 제거합니다. 이 함수는 장치와 통신해서는 안 됩니다.

매개변수:

- paths – 장치에 있는 책의 경로입니다.

- booklists – (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=’cardb’)`) 호출 결과를 담은 튜플입니다.

sync_booklists(booklists, end_session=True)[소스]

장치의 메타데이터를 업데이트합니다.

매개변수:

booklists – (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=’cardb’)`) 호출 결과를 담은 튜플입니다.

get_file(path, outfile, end_session=True)[소스]

장치의 path 에 있는 파일을 읽어 outfile 에 씁니다.

매개변수:

outfile – sys.stdout 같은 파일 객체 또는 open() 호출 결과입니다.

classmethod config_widget()[소스]

QWidget 를 반환해야 합니다. QWidget 에는 장치 인터페이스 설정이 포함됩니다.

classmethod save_settings(settings_widget)[소스]

설정을 디스크에 저장해야 합니다. config_widget() 가 만든 위젯을 받아 모든 설정을 디스크에 저장합니다.

classmethod settings()[소스]

opts 객체를 반환해야 합니다. opts 객체에는 최소한 장치의 형식 우선순위 목록인 format_map 속성이 있어야 합니다.

set_plugboards(plugboards, pb_func)[소스]

현재 plugboard 집합과 특정 plugboard 를 선택하는 함수를 드라이버에 제공합니다. 이 메서드는 add_books 및 sync_booklists 직전에 호출됩니다.

pb_func 는 다음 시그니처를 가진 호출 가능 객체입니다:

def pb_func(device_name, format, plugboards)

현재 장치 이름(클래스 이름 또는 DEVICE_PLUGBOARD_NAME), 관심 있는 형식(‘실제’ 형식 또는 ‘device_db’), 그리고 plugboards(set_plugboards 에서 받은 동일한 객체)를 전달합니다.

반환:

None 또는 단일 plugboard 인스턴스입니다.

set_driveinfo_name(location_code, name)[소스]

driveinfo 파일의 장치 이름을 ‘name’ 으로 설정합니다. 이 설정은 파일이 다시 생성되거나 이름이 다시 변경될 때까지 유지됩니다.

디스크가 아닌 장치는 get_device_information() 메서드가 반환하는 위치 코드에 따라 이 메서드를 구현해야 합니다.

prepare_addable_books(paths)[소스]

경로 목록을 받아, 책을 추가할 수 있는 버전의 경로 목록을 반환합니다.

책 준비 중 오류가 발생하면, 반환 목록에서 그 책의 위치에는 경로 대신 3-튜플 (original_path, 예외 인스턴스, traceback) 이 들어 있어야 합니다.

startup()[소스]

calibre 가 장치를 시작할 때 호출됩니다. 필요한 초기화를 수행하십시오. 이 클래스의 여러 인스턴스가 생성될 수 있으므로 __init__ 가 여러 번 호출될 수 있지만, 이 메서드는 그중 한 인스턴스에서만 호출됩니다. 이 메서드는 GUI 스레드가 아니라 장치 스레드에서 호출됩니다.

shutdown()[소스]

calibre 가 완전히 종료되거나 재시작 준비를 위해 종료될 때 호출됩니다. 필요한 정리 작업을 수행하십시오. 이 메서드는 GUI 스레드가 아니라 장치 스레드에서 호출됩니다.

get_device_uid()[소스]

현재 연결된 장치의 고유 id를 반환해야 합니다(open() 이 성공한 직후 호출됨). ASK_TO_ALLOW_CONNECT = True 로 설정했다면 이 메서드를 반드시 구현해야 합니다.

ignore_connected_device(uid)[소스]

앞으로 uid(get_device_uid() 호출 결과)로 식별된 장치를 무시해야 합니다. ASK_TO_ALLOW_CONNECT = True 로 설정했다면 이 메서드를 반드시 구현해야 합니다. 이 함수는 open() 직후 호출되므로, open() 이 어떤 상태를 캐시한다면 드라이버는 그 상태를 재설정해야 합니다.

get_user_blacklisted_devices()[소스]

사용자가 무시하도록 요청한 모든 장치에 대해 장치 uid 를 친숙한 이름으로 매핑한 값을 반환합니다.

set_user_blacklisted_devices(devices)[소스]

이 드라이버가 무시해야 할 장치 uid 목록을 설정합니다.

specialize_global_preferences(device_prefs)[소스]

장치가 특정 환경설정을 재정의하려는 경우 이 메서드를 구현하십시오. 재정의될 수 있는 환경설정을 원하는 모든 호출 지점이 prefs[‘something’] 대신 device_prefs[‘something’] 을 사용하도록 해야 합니다. 이 메서드는 device_prefs.set_overrides(pref=val, pref=val, …) 를 호출해야 합니다. 현재 사용되는 예: 메타데이터 관리 (prefs[‘manage_device_metadata’])

set_library_info(library_name, library_uuid, field_metadata)[소스]

현재 calibre 라이브러리에 대한 정보가 필요하다면 이 메서드를 구현하십시오. 이 메서드는 시작 시와 연결된 상태에서 calibre 라이브러리가 바뀔 때 호출됩니다.

is_dynamically_controllable()[소스]

장치 관리자가 플러그인을 시작할 때 호출됩니다. 이 메서드가 문자열을 반환하면 a) 장치 관리자의 동적 제어 인터페이스를 지원하고, b) 그 이름이 플러그인과 통신할 때 사용됩니다.

이 메서드는 GUI 스레드에서 호출될 수 있습니다. 따라서 이를 구현하는 드라이버는 스레드 안전해야 합니다.

start_plugin()[소스]

플러그인을 시작하기 위해 호출됩니다. 플러그인은 자신이 사용하는 방식으로 장치 연결을 받기 시작해야 합니다. 이미 연결을 받고 있다면 아무것도 하지 마십시오.

이 메서드는 GUI 스레드에서 호출될 수 있습니다. 따라서 이를 구현하는 드라이버는 스레드 안전해야 합니다.

stop_plugin()[소스]

플러그인을 중지하기 위해 호출됩니다. 플러그인은 더 이상 연결을 받지 않아야 하며, 자신이 사용한 리소스를 정리해야 합니다. 대개 이 메서드는 shutdown 을 호출해야 할 가능성이 높습니다. 이미 연결을 받고 있지 않다면 아무것도 하지 마십시오.

이 메서드는 GUI 스레드에서 호출될 수 있습니다. 따라서 이를 구현하는 드라이버는 스레드 안전해야 합니다.

get_option(opt_string, default=None)[소스]

opt_string 이 가리키는 옵션의 값을 반환합니다. 이 메서드는 플러그인이 시작되지 않은 상태에서도 호출될 수 있습니다. 옵션이 존재하지 않으면 None을 반환하십시오.

이 메서드는 GUI 스레드에서 호출될 수 있습니다. 따라서 이를 구현하는 드라이버는 스레드 안전해야 합니다.

set_option(opt_string, opt_value)[소스]

opt_string 이 가리키는 옵션의 값을 설정합니다. 이 메서드는 플러그인이 시작되지 않은 상태에서도 호출될 수 있습니다.

이 메서드는 GUI 스레드에서 호출될 수 있습니다. 따라서 이를 구현하는 드라이버는 스레드 안전해야 합니다.

is_running()[소스]

플러그인이 시작된 상태이면 True, 그렇지 않으면 false 를 반환합니다.

이 메서드는 GUI 스레드에서 호출될 수 있습니다. 따라서 이를 구현하는 드라이버는 스레드 안전해야 합니다.

synchronize_with_db(db, book_id, book_metadata, first_call)[소스]

장치에 있는 책이 calibre DB 의 책과 일치하는 것으로 판단될 때, 책 매칭 과정 중 호출됩니다. 필요하다면 장치에서 calibre DB 로 데이터를 동기화하는 책임은 이 메서드에 있습니다.

이 메서드는 두 값으로 이루어진 튜플을 반환해야 합니다. 첫 번째 값은 calibre 데이터베이스가 변경된 경우 변경된 calibre 책 id 의 집합이며, 데이터베이스가 변경되지 않았다면 None 입니다. 첫 번째 값이 빈 집합이면 장치에 있는 책의 메타데이터가 calibre 메타데이터로 업데이트되어 장치로 다시 전달되지만, 해당 책에 대한 GUI 새로 고침은 수행되지 않습니다. calibre 데이터가 올바르지만 이를 장치로 보내야 할 때 유용합니다.

두 번째 값 자체도 2값 튜플입니다. 그 안의 첫 번째 값은 책 형식을 장치로 보내야 하는지 여부를 지정합니다. 목적은 장치의 책이 calibre 의 책과 같은지 검증할 수 있게 하는 것입니다. 보낼 책이 없으면 이 값은 None 이어야 하며, 보내야 한다면 장치에서의 기본 파일 이름(예: foobar.epub)을 반환하십시오. 이름에는 반드시 확장자를 포함해야 합니다. 장치 하위 시스템은 None 이 아닌 값이 반환된 모든 책에 대해 send_books 작업을 생성합니다. 참고: 나중에 확장자를 가져오는 용도 외에는, 대부분의 장치가 파일 이름 생성에 템플릿을 사용하므로 이 이름은 무시됩니다. 반환된 튜플의 두 번째 값은 그 형식이 미래 날짜인지 여부를 나타냅니다. 그렇다면 True, 아니면 False를 반환하십시오. calibre 는 미래 날짜 책 목록을 사용자에게 보여 주는 대화상자를 표시합니다.

매우 중요: 이 메서드는 GUI 스레드에서 호출됩니다. 장치 관리자 스레드와 관련하여 스레드 안전해야 합니다.

book_id: 데이터베이스 안 책의 calibre id. book_metadata: 장치에서 온 책의 Metadata 객체. first_call: 동기화 중 첫 호출이면 True, 아니면 False

class calibre.devices.interface.BookList(oncard, prefix, settings)[소스]

기반 클래스: list

책 목록입니다. 각 Book 객체는 다음 필드를 가져야 합니다.

1. title

2. authors

3. size (책의 파일 크기)

4. 날짜시간 (UTC 시간 투플)

5. path (장치 안 책의 경로)

6. thumbnail (None 일 수 있음). thumbnail 은 이미지 데이터를 담은 str/bytes 객체이거나, 절대 경로(플랫폼 고유 형식)를 저장하는 image_path 속성을 가져야 합니다.

7. tags (비어 있을 수 있는 문자열 목록)

supports_collections()[소스]

장치가 이 책 목록에 대한 컬렉션을 지원하면 True를 반환합니다.

add_book(book, replace_metadata)[소스]

책을 booklist 에 추가합니다. 목적은 장치 내부 메타데이터를 유지하는 것입니다. booklists 를 동기화해야 한다면 True를 반환하십시오.

remove_book(book)[소스]

booklist 에서 책을 제거합니다. 동시에 장치 메타데이터도 수정하십시오.

get_collections(collection_attributes)[소스]

collection_attributes 로부터 생성된 컬렉션 사전을 반환합니다. 사전의 각 항목 형식은 컬렉션 이름:[책 목록] 입니다.

책 목록은 책 제목순으로 정렬되며, series 로부터 생성된 컬렉션의 경우에는 series_index 가 사용됩니다.

매개변수:

collection_attributes – Book 객체의 속성 목록

USB 대용량 저장소 기반 장치

이러한 장치의 기본 클래스는 calibre.devices.usbms.driver.USBMS 입니다. 이 클래스는 다시 아래에 문서화된 기반 클래스들로부터 일부 기능을 상속받습니다. 전형적인 기본 USBMS 기반 드라이버는 다음과 같습니다:

from calibre.devices.usbms.driver import USBMS

class PDNOVEL(USBMS):
name = 'Pandigital Novel device interface'
gui_name = 'PD Novel'
description = _('Communicate with the Pandigital Novel')
author = 'Kovid Goyal'
supported_platforms = ['windows', 'linux', 'osx']
FORMATS = ['epub', 'pdf']

VENDOR_ID = [0x18d1]
PRODUCT_ID = [0xb004]
BCD = [0x224]

THUMBNAIL_HEIGHT = 144

EBOOK_DIR_MAIN = 'eBooks'
SUPPORTS_SUB_DIRS = False

def upload_cover(self, path, filename, metadata):
coverdata = getattr(metadata, 'thumbnail', None)
if coverdata and coverdata[2]:
with open('%s.jpg' % os.path.join(path, filename), 'wb') as coverfile:
coverfile.write(coverdata[2])

class calibre.devices.usbms.device.Device(plugin_path)[소스]

기반 클래스: DeviceConfig, DevicePlugin

이 클래스는 자신을 USB 대용량 저장 장치로 노출하는 장치용 모든 드라이버에 공통된 논리를 제공합니다. 모든 플랫폼에서 USBMS 장치의 마운트/꺼내기 구현을 제공합니다.

VENDOR_ID = 0

VENDOR_ID 는 정수, 정수 목록 또는 사전이 될 수 있습니다. 사전인 경우에는 다음 형식의 사전들의 사전이어야 합니다:

{
integer_vendor_id : { product_id : [list of BCDs], ... },
...
}

PRODUCT_ID = 0

정수 또는 정수 목록

BCD = None

BCD 는 BCD를 기준으로 장치를 구분하지 않으려면 None 일 수 있고, 이 드라이버가 지원하는 모든 장치의 BCD 번호 목록일 수도 있습니다.

WINDOWS_MAIN_MEM = None

Windows PnP id 문자열에서 장치의 주 메모리를 식별하는 문자열입니다. None, 문자열, 문자열 목록 또는 컴파일된 정규식일 수 있습니다.

WINDOWS_CARD_A_MEM = None

Windows PnP id 문자열에서 장치의 첫 번째 카드를 식별하는 문자열입니다. None, 문자열, 문자열 목록 또는 컴파일된 정규식일 수 있습니다.

WINDOWS_CARD_B_MEM = None

Windows PnP id 문자열에서 장치의 두 번째 카드를 식별하는 문자열입니다. None, 문자열, 문자열 목록 또는 컴파일된 정규식일 수 있습니다.

OSX_MAIN_MEM_VOL_PAT = None

새 드라이버 감지에서 주 메모리와 저장 카드를 구분하는 데 사용됩니다. macOS 가 할당한 주 메모리 마운트 지점과 일치하는 정규식이어야 합니다.

MAX_PATH_LEN = 250

장치에 생성되는 경로의 최대 길이

NEWS_IN_FOLDER = True

뉴스를 전용 폴더에 넣기

classmethod model_metadata() → tuple[ModelMetadata, ...][소스]

이 드라이버가 지원하는 실제 장치 모델 전체에 대한 메타데이터입니다.

reset(key='-1', log_packets=False, report_progress=None, detected_device=None)[소스]

매개변수:

- key – 장치 잠금을 해제하기 위한 키

- log_packets – True이면 장치로 오가는 패킷 스트림이 기록됩니다.

- report_progress – 여러 작업에 대해 % 진행률(0에서 100 사이의 숫자)을 받아 호출되는 함수입니다. -1로 호출되면 해당 작업에는 진행률 정보가 없다는 뜻입니다.

- detected_device – 장치 스캐너에서 가져온 장치 정보입니다.

set_progress_reporter(report_progress)[소스]

진행률 정보를 보고할 함수를 설정합니다.

매개변수:

report_progress – 여러 작업에 대해 % 진행률(0에서 100 사이의 숫자)을 받아 호출되는 함수입니다. -1로 호출되면 해당 작업에는 진행률 정보가 없다는 뜻입니다.

card_prefix(end_session=True)[소스]

카드 경로 접두사의 2요소 목록을 반환합니다. 카드가 없으면 해당 카드의 접두사는 None 입니다. 예: (‘/place’, ‘/place2’) (None, ‘place2’) (‘place’, None) (None, None)

total_space(end_session=True)[소스]

마운트 지점의 총 사용 가능 공간을 가져옵니다:

1. 주 메모리

2. 메모리 카드 A

3. 메모리 카드 B

반환:

(1, 2, 3) 위치의 총 공간(바이트)으로 이루어진 3요소 목록입니다. 특정 장치에 이 위치가 없으면 0을 반환해야 합니다.

free_space(end_session=True)[소스]

마운트 지점의 남은 공간을 가져옵니다:

1. 주 메모리

2. 카드 A

3. 카드 B

반환:

(1, 2, 3) 위치의 남은 공간(바이트)으로 이루어진 3요소 목록입니다. 특정 장치에 이 위치가 없으면 -1을 반환해야 합니다.

windows_sort_drives(drives)[소스]

WINDOWS_CARD_NAME 기준으로 주 메모리와 저장 카드를 구분하지 못하는 장치에서 이를 구분하기 위해 호출됩니다. 예: EB600

can_handle_windows(usbdevice, debug=False)[소스]

이 드라이버가 해당 장치를 처리할 수 있는지 추가 검사하기 위한 선택적 메서드입니다. 처리할 수 없다면 False를 반환해야 합니다. 이 메서드는 vendor, product id, bcd 가 일치한 뒤에만 호출되므로 비교적 시간이 드는 검사를 수행할 수 있습니다. 기본 구현은 True를 반환합니다. 이 메서드는 Windows 에서만 호출됩니다. can_handle() 도 참조하십시오.

USBMS 기반 장치의 경우 이 메서드는 기본적으로 can_handle() 에 위임된다는 점에 유의하십시오. 따라서 USBMS 하위 클래스에서는 can_handle() 만 재정의하면 됩니다.

매개변수:

usbdevice – calibre.devices.winusb.scan_usb_devices() 가 반환한 usbdevice 입니다.

open(connected_device, library_uuid)[소스]

장치별 초기화를 수행합니다. 장치가 감지된 뒤이지만 장치와 통신하는 다른 함수보다 먼저 호출됩니다. 예를 들어 장치가 USB 대용량 저장소로 자신을 나타내는 경우, 이 메서드는 장치를 마운트하거나 자동 마운트된 경우 어디에 마운트되었는지 찾는 역할을 합니다. calibre.devices.usbms.device.Device.open() 메서드에는 USB 대용량 저장 장치에 대한 좋은 예시가 되는 구현이 있습니다.

이 메서드는 사용자에게 메시지를 표시하기 위해 OpenFeedback 예외를 발생시킬 수 있습니다.

매개변수:

- connected_device – 열려고 하는 장치입니다. (vendor id, product id, bcd, 제조사 이름, 제품명, 장치 일련 번호) 튜플입니다. 하지만 일부 장치에는 일련 번호가 없고, Windows 에서는 처음 세 필드만 있으며 나머지는 None 입니다.

- library_uuid – 현재 calibre 라이브러리의 UUID 입니다. 라이브러리가 없으면(예: 명령줄에서 사용할 때) None 일 수 있습니다.

eject()[소스]

OS에서 장치를 마운트 해제 / 꺼냅니다. 장치와 통신해야 하는 대기 중 GUI 작업이 있는지는 확인하지 않습니다.

참고: 이 메서드는 다른 장치 메서드들과 동일한 스레드에서 호출되지 않을 수 있습니다.

post_yank_cleanup()[소스]

사용자가 먼저 꺼내지 않고 장치를 갑자기 분리했을 때 호출됩니다.

sanitize_callback(path)[소스]

개별 장치 드라이버가 create_upload_path() 가 사용하는 경로 정리 방식을 재정의할 수 있도록 하는 콜백입니다.

filename_callback(default, mi)[소스]

드라이버가 create_upload_path() 가 설정한 기본 파일 이름을 바꿀 수 있도록 하는 콜백입니다.

sanitize_path_components(components)[소스]

장치에 업로드할 파일의 경로 구성 요소에 대해 장치별 정리를 수행합니다.

get_annotations(path_map)[소스]

path_map 을 장치에서 찾은 파일의 annotation_map 으로 해석합니다.

add_annotation_to_library(db, db_id, annotation)[소스]

calibre 라이브러리에 주석 추가

class calibre.devices.usbms.cli.CLI[소스]

class calibre.devices.usbms.driver.USBMS(plugin_path)[소스]

기반 클래스: CLI, Device

모든 USBMS 장치의 기본 클래스입니다. 메타데이터/캐싱 메타데이터/등을 전송/수신/업데이트하기 위한 논리를 구현합니다.

description = '전자책 리더와 통신합니다.'

이 플러그인이 하는 일을 설명하는 짧은 문자열입니다.

author = 'John Schember'

이 플러그인의 작성자입니다.

supported_platforms = ['windows', 'osx', 'linux']

이 플러그인이 동작하는 플랫폼 목록입니다. 예: ['windows', 'osx', 'linux']

booklist_class

:py:class:`~calibre.devices.usbms.books.BookList`의 별칭

book_class

:py:class:`~calibre.devices.usbms.books.Book`의 별칭

FORMATS = []

지원하는 형식의 정렬된 목록

CAN_SET_METADATA = []

책의 메타데이터를 GUI를 통해 설정할 수 있는지 여부입니다.

get_device_information(end_session=True)[소스]

장치에 장치 정보를 요청합니다. L{DeviceInfoQuery} 를 참조하십시오.

반환:

(장치 이름, 장치 버전, 장치의 소프트웨어 버전, MIME 형식) 튜플입니다. 선택적으로 다섯 번째 요소로 드라이브 정보 사전을 가질 수 있습니다. 예시는 usbms.driver 를 참조하십시오.

set_driveinfo_name(location_code, name)[소스]

driveinfo 파일의 장치 이름을 ‘name’ 으로 설정합니다. 이 설정은 파일이 다시 생성되거나 이름이 다시 변경될 때까지 유지됩니다.

디스크가 아닌 장치는 get_device_information() 메서드가 반환하는 위치 코드에 따라 이 메서드를 구현해야 합니다.

books(oncard=None, end_session=True)[소스]

장치에 있는 전자책 목록을 반환합니다.

매개변수:

oncard – ‘carda’ 또는 ‘cardb’ 이면 해당 저장 카드의 전자책 목록을 반환하고, 그렇지 않으면 장치의 주 메모리에 있는 전자책 목록을 반환합니다. 카드를 지정했는데 카드에 책이 없으면 빈 목록을 반환합니다.

반환:

BookList 입니다.

upload_books(files, names, on_card=None, end_session=True, metadata=None)[소스]

책 목록을 장치에 업로드합니다. 장치에 이미 파일이 있다면 교체해야 합니다. 장치에 여유 공간이 충분하지 않으면 이 메서드는 FreeSpaceError 를 발생시켜야 합니다. FreeSpaceError 의 텍스트에는 on_card 가 None 이 아니면 반드시 “card” 라는 단어가, 그렇지 않으면 반드시 “memory” 라는 단어가 포함되어야 합니다.

매개변수:

- files – 경로 목록

- names – 장치에 업로드된 뒤 책이 가지게 될 파일명 목록입니다. len(names) == len(files)

- metadata – None이 아니면 Metadata 객체 목록입니다. 아이디어는 이 메타데이터를 사용해 장치의 어느 위치에 책을 둘지 결정하는 것입니다. len(metadata) == len(files). 일반 표지(표지 경로) 외에 thumbnail 속성이 있을 수도 있으며, 있을 경우 이를 우선 사용해야 합니다. thumbnail 속성의 형식은 (너비, 높이, jpeg 형식의 cover_data) 입니다.

반환:

3요소 튜플 목록입니다. 이 목록은 add_books_to_metadata() 에 전달하기 위한 것입니다.

upload_cover(path, filename, metadata, filepath)[소스]

책 표지를 장치에 업로드합니다. 기본 구현은 아무 작업도 하지 않습니다.

매개변수:

- path – 연관된 책이 위치한 폴더의 전체 경로입니다.

- filename – 확장자를 제외한 책 파일 이름입니다.

- metadata – 책에 속한 메타데이터입니다. 표지에는 metadata.thumbnail 을 사용하십시오.

- filepath – 전자책 파일의 전체 경로입니다.

add_books_to_metadata(locations, metadata, booklists)[소스]

booklist 에 위치를 추가합니다. 이 함수는 장치와 통신해서는 안 됩니다.

매개변수:

- locations – L{upload_books} 호출 결과

- metadata – upload_books() 와 동일한 Metadata 객체 목록입니다.

- booklists – (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=’cardb’)`) 호출 결과를 담은 튜플입니다.

delete_books(paths, end_session=True)[소스]

장치 경로에 있는 책을 삭제합니다.

remove_books_from_metadata(paths, booklists)[소스]

메타데이터 목록에서 책을 제거합니다. 이 함수는 장치와 통신해서는 안 됩니다.

매개변수:

- paths – 장치에 있는 책의 경로입니다.

- booklists – (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=’cardb’)`) 호출 결과를 담은 튜플입니다.

sync_booklists(booklists, end_session=True)[소스]

장치의 메타데이터를 업데이트합니다.

매개변수:

booklists – (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=’cardb’)`) 호출 결과를 담은 튜플입니다.

classmethod normalize_path(path)[소스]

플랫폼 고유 경로 구분자를 사용한 경로를 반환합니다.

사용자 인터페이스 동작

ZIP 파일로 자체 플러그인을 추가하는 경우, InterfaceActionBase 와 InterfaceAction 을 모두 하위 클래스로 만들어야 합니다. InterfaceActionBase 하위 클래스의 load_actual_plugin() 메서드는 InterfaceAction 하위 클래스의 인스턴스화된 객체를 반환해야 합니다.

class calibre.gui2.actions.InterfaceAction(parent, site_customization)[소스]

기반 클래스: QObject

그래픽 사용자 인터페이스에서 수행할 수 있는 “동작” 을 나타내는 플러그인입니다. 도구 모음과 컨텍스트 메뉴의 모든 항목은 이 플러그인들로 구현됩니다.

이 클래스는 이러한 플러그인의 기본 클래스이지만, 플러그인을 calibre 플러그인 시스템에 통합하려면 실제 플러그인을 참조하는 래퍼 클래스를 만들어야 합니다. 예시는 calibre.customize.builtins 모듈을 참조하십시오.

두 InterfaceAction 객체의 이름이 같으면 우선순위가 높은 것이 우선합니다.

하위 클래스는 genesis(), library_changed(), location_selected(), shutting_down(), initialization_complete(), tag_browser_context_action() 메서드를 구현해야 합니다.

초기화가 끝나면 이 플러그인은 gui 멤버를 통해 calibre 메인 GUI 에 접근할 수 있습니다. 예를 들어 이름으로 다른 플러그인에 접근할 수 있습니다:

self.gui.iactions['Save To Disk']

실제 플러그인에 접근하려면 interface_action_base_plugin 속성을 사용하십시오. 이 속성은 플러그인이 초기화된 뒤에만 사용할 수 있습니다. do_user_config() 같은 플러그인 클래스 메서드를 사용하려 할 때 유용합니다.

action_spec 에 지정된 QAction 이 자동으로 생성되어 self.qaction 으로 제공됩니다.

name = 'Implement me'

플러그인 이름입니다. 동일한 이름의 플러그인이 둘 있으면 우선순위가 높은 것이 우선합니다.

priority = 1

플러그인 우선순위입니다. 동일한 이름의 플러그인이 둘 있으면 우선순위가 높은 것이 우선합니다.

popup_type = 1

이 플러그인이 도구 모음에 추가될 때 사용할 메뉴 팝업 유형입니다.

auto_repeat = False

이 동작의 단축키를 누르고 있는 동안 자동 반복할지 여부입니다.

action_spec = ('text', 'icon', None, None)

형식: (text, icon_path, tooltip, keyboard shortcut). icon, tooltip, keyboard shortcut 은 None 일 수 있습니다. keyboard shortcut 은 문자열, None 또는 단축키 튜플이어야 합니다. None 이면 이 동작에 대응하는 단축키가 등록되지 않습니다. 빈 튜플을 전달하면 기본 키 바인딩 없이 단축키가 등록됩니다.

action_shortcut_name = None

None 이 아니면, 위 action spec 에 대해 키보드 단축키를 사용자 지정할 때 action_spec[0] 대신 사용자에게 표시할 이름으로 사용됩니다.

action_add_menu = False

True이면 메뉴가 자동으로 생성되어 self.qaction 에 추가됩니다.

action_menu_clone_qaction = False

True이면 self.qaction 의 복제본이 self.qaction 의 메뉴에 추가됩니다. 이 동작의 텍스트를 self.qaction 과 다르게 하고 싶다면, 이 변수를 새 텍스트로 설정하십시오.

dont_add_to = frozenset({})

이 동작을 추가하면 안 되는 위치 집합입니다. 가능한 위치 목록은 all_locations 를 참조하십시오.

dont_remove_from = frozenset({})

이 동작을 제거하면 안 되는 위치 집합입니다. 가능한 위치 목록은 all_locations 를 참조하십시오.

action_type = 'global'

동작 유형입니다. ‘current’ 는 현재 보기에 대해 작동함을 의미하고, ‘global’ 은 현재 보기가 아니라 calibre 전체에 대해 작동하는 동작을 의미합니다.

accepts_drops = False

True이면 이 InterfaceAction 은 드래그 앤 드롭 이벤트와 상호작용할 기회를 가집니다. 자세한 내용은 accept_enter_event(), :meth`:accept_drag_move_event`, drop_event() 메서드를 참조하십시오.

accept_enter_event(event, mime_data)[소스]

이 인터페이스 동작이 드래그 이벤트를 처리할 수 있는 경우에만 True를 반환해야 합니다. event 에 대해 accept/ignore 를 호출하지 마십시오. 이는 calibre UI 가 처리합니다.

accept_drag_move_event(event, mime_data)[소스]

이 인터페이스 동작이 드래그 이벤트를 처리할 수 있는 경우에만 True를 반환해야 합니다. event 에 대해 accept/ignore 를 호출하지 마십시오. 이는 calibre UI 가 처리합니다.

drop_event(event, mime_data)[소스]

이 메서드는 유용한 동작을 수행한 뒤, 이 인터페이스 동작이 drop 이벤트를 처리할 수 있는 경우에만 True를 반환해야 합니다. event 에 대해 accept/ignore 를 호출하지 마십시오. 이는 calibre UI 가 처리합니다. 이 함수에서는 블로킹되거나 오래 걸리는 작업을 수행하지 마십시오. 대신 신호를 발생시키거나 QTimer.singleShot 을 사용하고 빠르게 반환하십시오. 예시는 내장 동작을 참조하십시오.

create_menu_action(menu, unique_name, text, icon=None, shortcut=None, description=None, triggered=None, shortcut_name=None, persist_shortcut=False)[소스]

QMenu 에 동작을 쉽게 추가하기 위한 편의 메서드입니다. 생성된 QAction 을 반환합니다. 이 동작에는 calibre_shortcut_unique_name 이라는 추가 속성이 있으며, None 이 아니면 이 동작이 키보드 관리자에 등록된 고유 이름을 가리킵니다.

매개변수:

- menu – 새로 생성된 동작이 추가될 QMenu

- unique_name – 이 동작의 고유 이름입니다. 전역적으로 고유해야 하므로 가능한 한 설명적으로 만드십시오. 확신이 서지 않으면 UUID 를 추가하십시오.

- text – 동작의 텍스트입니다.

- icon – QIcon 또는 파일 이름입니다. 파일 이름은 QIcon.ic() 내장 함수에 전달되므로 images 폴더의 전체 경로를 전달할 필요가 없습니다.

- shortcut – 문자열, 문자열 목록, None 또는 False 입니다. False 이면 이 동작에 대한 키보드 단축키가 등록되지 않습니다. None 이면 기본 키 바인딩이 없는 키보드 단축키가 등록됩니다. 문자열과 문자열 목록은 지정한 기본 키 바인딩으로 단축키를 등록합니다.

- description – 이 동작의 설명입니다. 도구 설명 설정에 사용됩니다.

- triggered – 생성된 동작의 triggered 시그널에 연결되는 호출 가능 객체입니다.

- shortcut_name – 이 동작의 키보드 단축키를 사용자 지정할 때 사용자에게 표시되는 텍스트입니다. 기본값은 text 값입니다.

- persist_shortcut – 항상 나타나지 않거나 라이브러리에 따라 달라지는 동작의 단축키는 `persist_shortcut` 을 True로 설정하지 않으면 다른 키보드 단축키를 수정할 때 사라질 수 있습니다.

load_resources(names)[소스]

이 플러그인이 ZIP 파일(사용자 추가 플러그인)로 제공되는 경우, 이 메서드를 사용해 ZIP 파일에서 리소스를 불러올 수 있습니다.

예를 들어 이미지를 불러오려면:

pixmap = QPixmap()
pixmap.loadFromData(tuple(self.load_resources(['images/icon.png']).values())[0])
icon = QIcon(pixmap)

매개변수:

names – / 를 구분자로 사용한 ZIP 파일 내부 리소스 경로 목록

반환:

{name : file_contents} 형태의 사전입니다. ZIP 파일에서 찾지 못한 이름은 사전에 포함되지 않습니다.

genesis()[소스]

이 플러그인을 설정합니다. 초기화 중 한 번만 호출됩니다. self.gui 를 사용할 수 있습니다. action_spec 에 지정된 동작은 self.qaction 으로 제공됩니다.

location_selected(loc)[소스]

calibre 에 표시되는 책 목록이 바뀔 때마다 호출됩니다. 현재 loc 의 값은 library, main, card and cardb 입니다.

이 메서드는 위치에 맞게 이 동작과 하위 동작들을 활성화/비활성화해야 합니다.

library_about_to_change(olddb, db)[소스]

현재 라이브러리가 바뀔 때마다 호출됩니다.

매개변수:

- olddb – 이전 라이브러리에 대응하는 LibraryDatabase 입니다.

- db – 새 라이브러리에 대응하는 LibraryDatabase 입니다.

library_changed(db)[소스]

현재 라이브러리가 바뀔 때마다 호출됩니다.

매개변수:

db – 현재 라이브러리에 대응하는 LibraryDatabase 입니다.

gui_layout_complete()[소스]

메인 GUI 의 레이아웃이 완료되면 각 동작마다 한 번 호출됩니다. 동작이 레이아웃을 변경해야 한다면 initialization_complete() 가 아니라 여기에서 수행해야 합니다.

initialization_complete()[소스]

메인 GUI 의 초기화가 완료되면 각 동작마다 한 번 호출됩니다.

tag_browser_context_action(index)[소스]

태그 브라우저에서 컨텍스트 메뉴를 표시할 때 호출됩니다. index 는 오른쪽 클릭한 태그 브라우저 항목을 가리키는 QModelIndex 입니다. index.valid() 로 유효성을 검사하고 index.data(Qt.ItemDataRole.UserRole) 로 기반 TagTreeItem 객체를 가져오십시오. 이 메서드가 yield 하는 모든 동작 객체는 컨텍스트 메뉴에 추가됩니다.

shutting_down()[소스]

메인 GUI 가 종료되는 과정에 있을 때 플러그인마다 한 번 호출됩니다. 사용한 리소스를 해제하되, 종료를 오래 막지 않도록 하십시오.

class calibre.customize.InterfaceActionBase(*args, **kwargs)[소스]

기반 클래스: Plugin

supported_platforms = ['windows', 'osx', 'linux']

이 플러그인이 동작하는 플랫폼 목록입니다. 예: ['windows', 'osx', 'linux']

author = 'Kovid Goyal'

이 플러그인의 작성자입니다.

type = '사용자 인터페이스 동작'

이 플러그인의 유형입니다. GUI에서 플러그인을 분류하는 데 사용됩니다.

load_actual_plugin(gui)[소스]

실제 인터페이스 동작 플러그인 객체를 반환해야 합니다.

환경설정 플러그인
class calibre.customize.PreferencesPlugin(plugin_path)[소스]

기반 클래스: Plugin

환경설정 대화상자에 표시되는 위젯을 나타내는 플러그인입니다.

이 플러그인에는 중요한 메서드가 하나 있는데, 바로 create_widget() 입니다. 플러그인의 여러 필드는 UI 에서의 분류 방식을 제어합니다.

supported_platforms = ['windows', 'osx', 'linux']

이 플러그인이 동작하는 플랫폼 목록입니다. 예: ['windows', 'osx', 'linux']

author = 'Kovid Goyal'

이 플러그인의 작성자입니다.

type = '환경설정'

이 플러그인의 유형입니다. GUI에서 플러그인을 분류하는 데 사용됩니다.

config_widget = None

ConfigWidgetInterface 를 구현하는 ConfigWidget 이라는 클래스가 들어 있는 모듈의 import 경로입니다. create_widget() 에서 사용됩니다.

category_order = 100

이 플러그인의 category 가 카테고리 목록 중 어디에 위치해야 하는지입니다.

name_order = 100

카테고리 내 이름 목록에서 이 플러그인의 gui_name 이 위치해야 하는 자리입니다.

category = None

이 플러그인이 속해야 하는 카테고리입니다.

gui_category = None

이 플러그인에 대해 사용자에게 표시되는 카테고리 이름입니다.

gui_name = None

이 플러그인에 대해 사용자에게 표시되는 이름입니다.

icon = None

이 플러그인의 아이콘이며 절대 경로여야 합니다.

description = None

도구 설명 등에 사용되는 설명입니다.

create_widget(parent=None)[소스]

이 환경설정 그룹을 설정하는 데 사용되는 실제 Qt 위젯을 만들고 반환합니다. 위젯은 calibre.gui2.preferences.ConfigWidgetInterface 를 구현해야 합니다.

기본 구현은 config_widget 를 사용해 위젯을 인스턴스화합니다.

class calibre.gui2.preferences.ConfigWidgetInterface[소스]

이 클래스는 환경설정 대화상자에 표시되는 모든 위젯이 구현해야 하는 인터페이스를 정의합니다. 이 인터페이스를 구현하고 여러 편의 메서드도 정의한 기본 클래스는 ConfigWidgetBase 를 참조하십시오.

changed_signal = None

사용자가 이 위젯에서 값을 변경할 때마다 이 시그널을 발생시켜야 합니다.

supports_restoring_to_defaults = True

restore_to_defaults() 메서드가 구현되어 있는 경우에만 True로 설정하십시오.

restore_defaults_desc = '설정을 기본값으로 복원합니다. 실제로 기본 설정을 저장하려면 적용을 클릭하여야 합니다.'

“기본값 복원” 버튼의 도구 설명입니다.

restart_critical = False

True이면 환경설정 대화상자는 사용자가 더 이상의 환경설정을 설정하지 못하게 합니다. commit() 이 True를 반환할 때만 효과가 있습니다.

genesis(gui)[소스]

위젯이 표시되기 전에 한 번 호출되며, 필요한 설정을 수행해야 합니다.

매개변수:

gui – calibre 메인 그래픽 사용자 인터페이스

initialize()[소스]

모든 설정 값을 초기값(설정 파일에 저장된 값)으로 되돌려야 합니다. “return” 문은 선택 사항입니다. 대화상자를 표시하지 않으려면 False를 반환하십시오.

restore_defaults()[소스]

모든 설정 값을 기본값으로 설정해야 합니다.

commit()[소스]

변경된 설정을 저장합니다. 변경에 재시작이 필요하면 True, 그렇지 않으면 False를 반환합니다. 오류가 발생했음을 나타내려면 AbortCommit 예외를 발생시키십시오. 오류가 무엇이고 어떻게 수정해야 하는지는 사용자가 알 수 있도록 직접 알려주어야 합니다.

refresh_gui(gui)[소스]

이 위젯이 커밋된 뒤 한 번 호출됩니다. GUI 가 변경된 설정을 다시 읽도록 만드는 역할을 합니다. 기본적으로 GUI 는 여러 요소를 다시 초기화하므로, 대부분의 위젯은 이 메서드를 사용할 필요가 없습니다.

initial_tab_changed()[소스]

처음 표시될 탭이 위젯이 보이기 전, 하지만 초기화된 뒤에 변경되면 호출됩니다.

class calibre.gui2.preferences.ConfigWidgetBase(parent=None)[소스]

체크박스, 콤보 상자, 텍스트 필드 등 표준 설정 위젯을 쉽게 추가할 수 있는 코드를 담은 기본 클래스입니다. register() 메서드를 참조하십시오.

이 클래스는 등록된 설정에 대해 변경 알림, 기본값 복원, GUI 객체와 설정 객체 간 변환 등을 자동으로 처리합니다.

설정 위젯이 이 클래스를 상속하지만 등록되지 않은 설정도 포함한다면, ConfigWidgetInterface 메서드를 재정의하고 그 안에서 기본 클래스 메서드를 호출해야 합니다.

changed_signal

사용자가 이 위젯에서 값을 변경할 때마다 이 시그널을 발생시켜야 합니다.

supports_restoring_to_defaults = True

restore_to_defaults() 메서드가 구현되어 있는 경우에만 True로 설정하십시오.

restart_critical = False

True이면 환경설정 대화상자는 사용자가 더 이상의 환경설정을 설정하지 못하게 합니다. commit() 이 True를 반환할 때만 효과가 있습니다.

register(name, config_obj, gui_name=None, choices=None, restart_required=False, empty_string_is_None=True, setting=<class 'calibre.gui2.preferences.Setting'>)[소스]

설정을 등록합니다.

매개변수:

- name – 설정 이름

- config_obj – 이 설정을 읽고 쓰는 설정 객체

- gui_name – 설정을 변경하는 인터페이스를 제공하는 GUI 객체의 이름입니다. 기본값은 'opt_' + name 으로 가정합니다.

- choices – 이 설정이 다중 선택(콤보박스) 기반 설정이라면, 선택지 목록입니다. 목록은 [(gui name, value), ...] 형식의 두 요소 튜플 목록입니다.

- setting – 이 설정을 관리하는 클래스입니다. 기본 클래스가 거의 모든 경우를 처리하므로, 이 매개변수는 드물게만 사용됩니다.

initialize()[소스]

모든 설정 값을 초기값(설정 파일에 저장된 값)으로 되돌려야 합니다. “return” 문은 선택 사항입니다. 대화상자를 표시하지 않으려면 False를 반환하십시오.

commit(*args)[소스]

변경된 설정을 저장합니다. 변경에 재시작이 필요하면 True, 그렇지 않으면 False를 반환합니다. 오류가 발생했음을 나타내려면 AbortCommit 예외를 발생시키십시오. 오류가 무엇이고 어떻게 수정해야 하는지는 사용자가 알 수 있도록 직접 알려주어야 합니다.

restore_defaults(*args)[소스]

모든 설정 값을 기본값으로 설정해야 합니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 명령행 인터페이스

명령행 인터페이스

참고
macOS에서 명령줄 도구는 calibre 번들 내부에 있습니다. 예를 들어 calibre를 /Applications`에 설치했다면 명령줄 도구는 :file:/Applications/calibre.app/Contents/MacOS/에 있습니다. 따라서 예를 들어 :file:`ebook-convert`를 실행하려면 :file:/Applications/calibre.app/Contents/MacOS/ebook-convert`를 사용합니다.

문서화된 명령어

- calibre
- calibre-customize
- calibre-debug
- calibre-server
- calibre-smtp
- calibredb
- ebook-convert
- ebook-edit
- ebook-meta
- ebook-polish
- ebook-viewer
- fetch-ebook-metadata
- lrf2lrs
- lrfviewer
- lrs2lrf
- web2disk

비문서화된 명령어

- ebook-device

- markdown-calibre

문서화되지 않은 명령은 터미널에서 인수 없이 실행하면 사용법을 확인할 수 있습니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 명령행 인터페이스 »
- calibre

calibre

calibre [options] [path_to_ebook 또는 calibre url ...]

calibre`의 메인 그래픽 사용자 인터페이스를 실행하고 선택적으로 path_to_ebook에 있는 전자책을 데이터베이스에 추가합니다. 책 추가 외에도 :command:`calibre URL을 지정해 다양한 동작을 수행할 수 있습니다. 예를 들어:

calibre://view-book/test_library/1842/epub

위 URL은 라이브러리 “test_library”에서 id 1842인 EPUB 형식의 책을 calibre 전자책 뷰어로 엽니다. 라이브러리 이름은 공백을 밑줄로 바꾼 라이브러리 폴더 이름입니다. 다양한 URL 기반 동작에 대한 전체 설명은 사용 설명서에 있습니다.

:command:`calibre`에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

[옵션]

--detach

제어 터미널이 있으면 분리합니다 (Linux 전용)

--help, -h

이 도움말 메시지를 표시한 후 종료

--ignore-plugins

사용자 지정 플러그인을 무시합니다. calibre 시작을 막는 플러그인을 설치한 경우 유용합니다

--no-update-check

업데이트를 확인하지 않음

--shutdown-running-calibre, -s

실행 중인 캘리버 인스턴스가 있다면 종료시킵니다. 만일 실행 중인 작업이 있다면 조용히 처리가 중단되니 조심해서 사용하도록 하십시오.

--start-in-tray

시스템 트레이 영역으로 최소화하여 시작합니다.

--verbose, -v

무시되며 사용하지 마십시오. 이전 버전 호환성 때문에만 존재합니다

--version

프로그램의 버전을 표시한 후 종료합니다.

--with-library

지정한 경로에 위치한 라이브러리를 사용합니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 명령행 인터페이스 »
- calibre-customize

calibre-customize

calibre-customize options

외부 플러그인을 불러들여 캘리버를 사용자 정의합니다.

:command:`calibre-customize`에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

[옵션]

--add-plugin, -a

플러그인을 포함하고 있는 ZIP 파일의 경로를 지정하여 플러그인을 추가합니다

--build-plugin, -b

플러그인 개발자용: 현재 플러그인을 개발 중인 폴더의 경로입니다. 이 명령은 플러그인을 자동으로 zip으로 묶고 calibre에 업데이트합니다.

--customize-plugin

플러그인 사용자 지정을 설정합니다. 플러그인 이름과 사용자 지정 문자열을 쉼표로 구분해 지정하십시오. 사용자 지정 문자열은 calibre 기본 GUI에서 플러그인을 사용자 지정할 때 입력하는 문자열과 같습니다.

--disable-plugin

주어진 이름의 플러그인을 사용하지 않습니다

--enable-plugin

주어진 이름의 플러그인을 사용합니다

--help, -h

이 도움말 메시지를 표시한 후 종료

--list-plugins, -l

설치된 플러그인을 나열합니다.

--remove-plugin, -r

이름으로 사용자 지정 플러그인을 제거합니다. 내장된 플러그인에 영향을 미치지 않습니다

--version

프로그램의 버전을 표시한 후 종료합니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 명령행 인터페이스 »
- calibre-debug

calibre-debug

calibre-debug [옵션]

calibre 디버깅에 유용한 다양한 명령줄 인터페이스입니다. 옵션 없이 실행하면 내장된 Python 인터프리터를 시작합니다. 또한 calibre 기본 GUI, calibre 전자책 뷰어, calibre 편집기를 디버그 모드로 실행할 수도 있습니다.

이 명령에는 전용 명령줄 도구가 없는 calibre 기능들, 예를 들면 글꼴 서브셋 생성, 전자책 비교 도구 같은 기능에 대한 인터페이스도 포함되어 있습니다.

또한 calibre-debug 를 사용해 독립 실행형 스크립트를 실행할 수도 있습니다. 사용 방법은 다음과 같습니다:

calibre-debug -e myscript.py -- --option1 --option2 file1 file2 …

– 뒤의 모든 내용은 스크립트로 전달됩니다. 스크립트에서 calibre-debug 를 shebang 으로 사용할 수도 있습니다. 예를 들면 다음과 같습니다:

#!/usr/bin/env -S calibre-debug -e -- –

:command:`calibre-debug`에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

[옵션]

--add-simple-plugin

플러그인 코드가 담긴 py 파일에 대한 경로를 지정해서, 간단한 플러그인(즉, .py 파일로만 구성된 플러그인)을 추가합니다.

--command, -c

Python 코드를 실행합니다.

--debug-device-driver, -d

장치 감지 디버그

--default-programs

캘리버를 Windows 기본 프로그램에 등록/제거 합니다. --default-programs = (register|unregister)

--diff

캘리버의 diff 도구를 실행합니다. 예: calibre-debug --diff 파일1 파일2

--edit-book

캘리버 "책 편집" 도구를 디버그 모드로 실행합니다. -g와 마찬가지로 명령줄 인수를 전달하려면 –를 사용하세요.

--exec-file, -e

파일의 Python 코드를 실행합니다.

--explode-book, -x

책을 지정된 폴더로 분해합니다. 사용법: -x file.epub output_dir 표준 HTML 편집 도구를 사용하여 편집할 수 있는 HTML 파일 및 메타데이터 모음으로 책을 내보냅니다. EPUB, AZW3, HTMLZ 및 DOCX 파일과 함께 작동합니다.

--export-all-calibre-data

모든 calibre 데이터(책/설정/플러그인)를 내보냅니다. 보통은 내보내기 폴더와 내보낼 라이브러리를 묻습니다. 질문을 건너뛰려면 명령줄 인수로 직접 지정할 수도 있습니다. 내보내기 폴더와 라이브러리에는 절대 경로를 사용하십시오. 특수 키워드 "all" 을 사용하면 모든 라이브러리를 내보낼 수 있습니다. 예: calibre-debug --export-all-calibre-data # 대화형 사용 calibre-debug --export-all-calibre-data /path/to/empty/export/folder /path/to/library/folder1 /path/to/library2 calibre-debug --export-all-calibre-data /export/folder all # 알려진 모든 라이브러리 내보내기

--gui, -g

디버깅을 활성화한 상태로 GUI를 실행합니다. 디버그 출력은 stdout과 stderr로 표시됩니다. 명령줄 인수를 전달하려면 -- 뒤에 인수를 붙여 사용하십시오. 예: calibre-debug -g -- /path/to/ebook

--gui-debug

지정한 경로에 로그를 기록하면서 GUI를 디버그 콘솔과 함께 실행합니다. 내부 용도 전용이며, GUI를 디버그 모드로 실행하려면 -g 옵션을 사용하십시오.

--help, -h

이 도움말 메시지를 표시한 후 종료

--implode-book, -i

이전에 분해한 책을 다시 합칩니다. 사용법: -i output_dir file.epub 이전의 --explode-book 호출로 생성된 output_dir 의 파일들로부터 책을 가져옵니다. 분해할 때 사용한 것과 같은 파일 형식을 지정해야 합니다.

--import-calibre-data

이전에 내보낸 캘리버 데이터 가져오기

--inspect-mobi, -m

지정된 경로의 MOBI 파일을 검사합니다.

--kepubify

전체 변환을 수행하지 않고 지정한 EPUB 파일을 KEPUB로 변환합니다. Kobo 드라이버가 파일을 장치로 보낼 때 수행하는 방식입니다.

--paths

캘리버 환경을 설정하는데 필요한 경로들을 출력합니다.

--run-plugin, -r

명령줄 인터페이스를 제공하는 플러그인을 실행합니다. 예: calibre-debug -r "플러그인 이름" -- file1 --option1 -- 뒤의 모든 내용은 인수로 플러그인에 전달됩니다.

--run-test, -t

디버깅을 활성화한 상태로 GUI를 실행합니다. 디버그 출력은 stdout과 stderr에 기록됩니다. 명령줄 인수를 전달하려면 -- 뒤에 인수를 붙이십시오. 예: calibre-debug -g -- /path/to/ebook

--run-without-debug

DEBUG 플래그를 설정한 채 실행하지 마세요

--shutdown-running-calibre, -s

실행 중인 캘리버 인스턴스가 있다면 종료시킵니다. 만일 실행 중인 작업이 있다면 조용히 처리가 중단되니 조심해서 사용하도록 하십시오.

--subset-font, -f

지정된 글꼴에서 실제 사용된 문자만 취합니다. Font subsetting 프로그램에 옵션을 전달하려면 이 옵션 다음에 -- 를 사용하세요.

--test-build

빌드 시 바이너리 모듈을 검사합니다.

--un-kepubify

전체 변환을 수행하지 않고 지정한 KEPUB 파일을 EPUB로 변환합니다. Kobo 드라이버가 장치에서 파일을 가져올 때 수행하는 방식입니다.

--version

프로그램의 버전을 표시한 후 종료합니다.

--viewer, -w

전자책 뷰어를 디버그 모드로 실행합니다. -g와 마찬가지로 명령줄 인수를 전달하려면 –를 사용하세요.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 명령행 인터페이스 »
- calibre-server

calibre-server

calibre-server [options] [path to library folder...]

calibre 콘텐츠 서버를 시작합니다. calibre 콘텐츠 서버는 calibre 라이브러리를 인터넷을 통해 제공합니다. 라이브러리 폴더 경로를 :command:`calibre-server`의 인수로 지정할 수 있습니다. 경로를 지정하지 않으면 기본 calibre 프로그램이 알고 있는 모든 라이브러리가 사용됩니다.

:command:`calibre-server`에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

[옵션]

--access-log

접근 로그 파일 경로입니다. 이 로그에는 서버에 접속하여 요청을 수행한 클라이언트 정보가 기록됩니다. 기본적으로 접근 로그는 기록하지 않습니다.

--ajax-timeout

쿼리 전송 후 서버 응답을 기다리는 시간(초).

--auth-mode

사용할 인증 방식 선택. 서버에서 사용할 HTTP 인증 모드를 설정합니다. 이 서버를 SSL 프록시 뒤에 둘 경우 "basic"으로 설정하십시오. 그렇지 않으면 "auto"로 두면 되며, SSL이 구성된 경우 "basic"을, 그렇지 않으면 "digest"를 사용합니다.

--auto-reload

소스 코드가 바뀌면 서버를 자동으로 다시 불러옵니다. 개발 시 유용합니다. 종료 대기 시간에도 작은 값을 지정하는 것이 좋습니다.

--ban-after

차단을 위한 로그인 실패 횟수. IP 주소를 차단할 로그인 실패 횟수입니다

--ban-for

로그인 실패가 반복된 IP 주소 차단. 로그인 실패가 반복된 IP 주소의 접근을 지정한 분 수 동안 임시 차단합니다. 비밀번호 추측 시도를 막는 데 유용합니다. 0으로 설정하면 차단하지 않습니다.

--book-list-mode

기본 책 목록 모드 선택. 새 사용자에게 적용할 기본 책 목록 모드를 설정합니다. 개별 사용자는 자기 설정에서 이 기본값을 덮어쓸 수 있습니다. 기본값은 표지 격자입니다.

--compress-min-size

응답에 데이터 압축을 사용할 최소 크기(바이트).

--custom-list-template

사용자 정의 책 목록 모드용 템플릿이 들어 있는 JSON 파일의 경로입니다. 이런 템플릿 파일을 만드는 가장 쉬운 방법은 calibre의 환경설정-> 네트워크 공유-> 책 목록 템플릿으로 가서 템플릿을 만든 뒤 내보내는 것입니다.

--daemonize

프로세스를 백그라운드에서 데몬으로 실행(Linux 전용).

--displayed-fields

표시할 사용자 정의 필드 제한. 콘텐츠 서버의 /opds 및 /mobile 보기에서 표시할 사용자 정의 메타데이터 필드의 쉼표 구분 목록입니다. 이 옵션을 지정하면 이 목록에 없는 필드는 표시되지 않습니다. 예: my_rating,my_tags

--enable-allow-socket-preallocation, --disable-allow-socket-preallocation

예를 들어 systemd 소켓 활성화와 같은 소켓 사전 할당. 기본적으로 이 옵션은 활성화되어 있습니다.

--enable-auth, --disable-auth

서버 접근용 비밀번호 기반 인증. 기본적으로 서버는 제한 없이 누구나 접근할 수 있습니다. 이 옵션으로 미리 정의한 사용자로 접근을 제한할 수 있습니다. 기본적으로 이 옵션은 비활성화되어 있습니다.

--enable-fallback-to-detected-interface, --disable-fallback-to-detected-interface

자동 감지된 인터페이스로 대체. 어떤 이유로든 서버가 listen_on 옵션에 지정된 인터페이스에 바인딩할 수 없으면, 외부 네트워크와 연결되는 인터페이스를 감지해 그쪽에 바인딩을 시도합니다. 기본적으로 이 옵션은 활성화되어 있습니다.

--enable-local-write, --disable-local-write

인증되지 않은 로컬 연결의 변경 허용. 기본적으로 인증을 켜지 않으면 서버는 읽기 전용 모드로 동작하여 익명 사용자가 calibre 라이브러리를 변경하지 못하게 합니다. 이 옵션을 사용하면 서버가 실행 중인 같은 컴퓨터에서 연결하는 누구나 변경할 수 있습니다. 인증 없이 서버를 실행하면서도 calibredb로 calibre 라이브러리를 변경하려는 경우 유용합니다. 이 옵션을 켜면 컴퓨터에서 실행 중인 어떤 프로그램이든 calibre 라이브러리를 변경할 수 있다는 점에 유의하십시오. 특히 Firefox 웹 브라우저는 임의의 웹사이트에도 이를 허용합니다. 기본적으로 이 옵션은 비활성화되어 있습니다.

--enable-log-not-found, --disable-log-not-found

HTTP 404(찾을 수 없음) 요청 기록. 기본적으로 서버는 찾을 수 없는 리소스에 대한 모든 HTTP 요청을 기록합니다. 서버가 봇의 대상이 되면 로그가 과도하게 쌓일 수 있습니다. 이 옵션으로 끌 수 있습니다. 기본적으로 이 옵션은 활성화되어 있습니다.

--enable-use-bonjour, --disable-use-bonjour

Bonjour를 통해 OPDS 피드 광고. Bonjour 서비스를 통해 OPDS 피드를 알리므로, OPDS 기반 읽기 앱이 서버를 자동으로 감지하고 연결할 수 있습니다. 기본적으로 이 옵션은 활성화되어 있습니다.

--enable-use-sendfile, --disable-use-sendfile

성능 향상을 위한 제로 카피 파일 전송. 네트워크로 파일을 보낼 때 커널 내 제로 카피 전송을 사용해 성능을 높입니다. 하지만 일부 문제가 있는 파일 시스템에서는 전송 파일이 손상될 수 있습니다. 파일 전송 손상이 발생하면 이 옵션을 끄십시오. 기본적으로 이 옵션은 활성화되어 있습니다.

--help, -h

이 도움말 메시지를 표시한 후 종료

--ignored-fields

무시할 사용자 정의 메타데이터 필드. 콘텐츠 서버의 /opds 및 /mobile 보기에서 표시하지 않을 사용자 정의 메타데이터 필드의 쉼표 구분 목록입니다. 예: my_rating,my_tags

--listen-on

연결을 수신할 인터페이스. 기본값은 사용 가능한 모든 IPv6 및 IPv4 인터페이스에서 수신하는 것입니다. 예를 들어 로컬 시스템의 IPv4 연결만 수신하려면 "127.0.0.1"로, 모든 들어오는 IPv4 연결을 수신하려면 "0.0.0.0"으로 바꿀 수 있습니다.

--log

서버 로그용 로그 파일 경로입니다. 이 로그에는 서버 정보와 오류가 기록되며, 접근 로그는 포함되지 않습니다. 기본값은 stdout에 기록하는 것입니다.

--manage-users

이 서버에 연결할 수 있는 사용자 데이터베이스를 관리합니다. –를 추가하여 자동화 모드로 사용할 수 있습니다. 자세한 내용은 calibre-server --manage-users -- help을 참고하십시오. --userdb 옵션도 함께 보십시오.

--max-header-line-size

단일 HTTP 헤더의 최대 크기(KB).

--max-job-time

작업자 프로세스 최대 실행 시간. 작업자 프로세스가 실행될 수 있는 최대 시간(분)입니다. 0으로 설정하면 제한이 없습니다.

--max-jobs

작업자 프로세스 최대 수. 작업자 프로세스는 필요할 때 시작되며 책 보기 준비, 책 추가, 변환 같은 큰 작업에 사용됩니다. 일반적으로 이러한 프로세스의 최대 수는 CPU 코어 수를 기준으로 결정됩니다. 이 설정으로 제어할 수 있습니다.

--max-log-size

로그 파일 최대 크기(MB). 서버가 생성하는 로그 파일의 최대 크기입니다. 로그가 이 크기를 넘으면 자동으로 순환됩니다. 0으로 설정하면 로그 순환이 비활성화됩니다.

--max-opds-items

OPDS 피드의 최대 책 수. 서버가 단일 OPDS 수집 피드에서 반환할 최대 책 수입니다.

--max-opds-ungrouped-items

OPDS 피드에서 그룹화되지 않은 항목의 최대 수. 저자/태그 같은 범주의 항목 수가 이 값보다 많으면 첫 글자 기준으로 그룹화합니다. 0으로 설정하면 비활성화됩니다.

--max-request-body-size

서버에 업로드할 수 있는 파일의 최대 크기(MB).

--num-per-page

한 페이지에 표시할 책 수. 브라우저에서 한 페이지에 표시할 책 수입니다.

--pidfile

프로세스 PID를 지정한 파일에 기록

--port

접속 대기용으로 사용할 서버측 포트 번호:.

--search-the-net-urls

"인터넷 검색" 기능용 URL이 들어 있는 JSON 파일 경로입니다. 이런 파일을 만드는 가장 쉬운 방법은 calibre의 환경설정-> 네트워크 공유-> 인터넷 검색으로 가서 URL을 만든 뒤 내보내는 것입니다.

--shutdown-timeout

정상 종료를 기다리는 총 시간(초).

--ssl-certfile

SSL 인증서 파일 경로.

--ssl-keyfile

SSL 개인 키 파일 경로.

--timeout

유휴 연결을 닫기까지의 시간(초).

--trusted-ips

특정 IP 주소의 인증되지 않은 연결에 변경 허용. 기본적으로 인증을 켜지 않으면 서버는 읽기 전용 모드로 동작하여 익명 사용자가 calibre 라이브러리를 변경하지 못하게 합니다. 이 옵션을 사용하면 지정한 IP 주소에서 연결하는 누구나 변경할 수 있습니다. 주소 또는 네트워크 규격의 쉼표로 구분된 목록이어야 합니다. 인증 없이 서버를 실행하면서도 calibredb로 calibre 라이브러리를 변경하려는 경우 유용합니다. 이 옵션을 켜면 지정한 IP 주소에서 연결하는 누구나 calibre 라이브러리를 변경할 수 있습니다.

--url-prefix

모든 URL 앞에 붙일 접두어. 이 서버를 리버스 프록시 뒤에서 실행하려는 경우 유용합니다. 예를 들어 URL 접두어로 /calibre를 사용하십시오.

--userdb

인증에 사용할 사용자 데이터베이스 파일 경로입니다. 데이터베이스는 SQLite 파일입니다. 만들려면 :option:`–manage-users`을 사용하십시오. 사용자 관리에 대한 자세한 내용: https://manual.calibre-ebook.com/ko/server.html#managing-user-accounts-from-the-command-line-only

--version

프로그램의 버전을 표시한 후 종료합니다.

--worker-count

요청 처리에 사용할 작업자 스레드 수.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 명령행 인터페이스 »
- calibre-smtp

calibre-smtp

calibre-smtp [options] [from to text]

SMTP 프로토콜을 사용해 메일을 보냅니다. :command:`calibre-smtp`에는 두 가지 동작 모드가 있습니다. 작성 모드에서는 from, to, text를 지정하며, 이것들로 이메일 메시지를 만들어 보냅니다. 필터 모드에서는 :command:`calibre-smtp`가 STDIN에서 완전한 이메일 메시지를 읽어 전송합니다.

text는 이메일 메시지 본문입니다.
text를 지정하지 않으면 STDIN에서 완전한 이메일 메시지를 읽습니다.
from은 발신자 이메일 주소이고 to는 수신자 이메일 주소입니다.
STDIN에서 완전한 이메일을 읽을 때는 from과 to는 SMTP 협상에만 사용되며 메시지 헤더는 수정되지 않습니다.

:command:`calibre-smtp`에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

[옵션]

--fork, -f

포크하여 백그라운드에서 메시지를 전달합니다. 이 옵션을 사용할 경우 전달 실패 처리를 위해 :option:`–outbox`도 함께 사용하는 것이 좋습니다.

--help, -h

이 도움말 메시지를 표시한 후 종료

--localhost, -l

localhost의 호스트 이름입니다. SMTP 서버에 연결할 때 사용합니다.

--outbox, -o

실패한 이메일 메시지를 저장할 maildir 폴더 경로입니다.

--timeout, -t

연결 대기시간 초과

--verbose, -v

더 자세히 출력

--version

프로그램의 버전을 표시한 후 종료합니다.

COMPOSE MAIL

이메일 작성 옵션입니다. 텍스트를 지정하지 않으면 무시됩니다

--attachment, -a

이메일에 첨부할 파일

--subject, -s

이메일 제목

SMTP RELAY

메일 전송에 SMTP 릴레이 서버를 사용하는 옵션입니다. –relay가 지정되지 않으면 calibre는 이메일을 직접 전송하려고 시도합니다.

--cafile

TLS 사용 시 서버 인증서를 검증하는 데 사용할 PEM 형식 CA 인증서 연결 파일의 경로입니다. 기본적으로는 시스템 CA 인증서를 사용합니다.

--dont-verify-server-certificate

TLS로 연결할 때 서버 인증서를 검증하지 않습니다. calibre 3.27 이전 버전에서는 이것이 기본 동작이었습니다. 자체 서명되었거나 그 밖에 유효하지 않은 인증서를 사용하는 릴레이를 쓴다면 이 옵션으로 3.27 이전 동작을 복원할 수 있습니다.

--encryption-method, -e

릴레이에 연결할 때 사용할 암호화 방식입니다. 선택지는 TLS, SSL, NONE이며 기본값은 TLS입니다. 경고: NONE을 선택하면 매우 안전하지 않습니다.

--password, -p

릴레이 비밀번호

--port

릴레이 서버에 연결할 포트입니다. 기본값은 암호화 방식이 SSL이면 465, 그렇지 않으면 25입니다.

--relay, -r

메일 전송에 사용할 SMTP 릴레이 서버입니다.

--username, -u

릴레이 사용자 이름

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 명령행 인터페이스 »
- calibredb

calibredb

calibredb command [options] [arguments]

:command:`calibredb`는 calibre 데이터베이스의 명령줄 인터페이스입니다. 여기에는 아래에 문서화된 여러 하위 명령이 있습니다.

calibredb`는 경로로 지정된 calibre 데이터베이스나, 로컬 컴퓨터 또는 인터넷을 통해 실행 중인 calibre :guilabel:`콘텐츠 서버`를 조작하는 데 사용할 수 있습니다. calibre
:guilabel:`콘텐츠 서버`는 :command:`calibre-server 프로그램을 사용하거나, calibre 메인 프로그램에서 :guilabel:`연결/공유 -> 콘텐츠 서버 시작`을 클릭하여 시작할 수 있습니다. :command:`calibredb`는 calibre 라이브러리를 변경할 수 있으므로, 먼저 서버에 인증을 설정해야 합니다.
이를 위한 방법은 두 가지입니다.

- 같은 컴퓨터에서 실행 중인 서버에만 연결할 계획이라면,

콘텐츠 서버의 --enable-local-write 옵션을 사용하여 로컬 컴퓨터에서 실행 중인 모든 프로그램(calibredb 포함)이 calibre 데이터에 변경을 가할 수 있도록 하면 됩니다. 메인 calibre 프로그램에서 서버를 실행하는 경우, 이 옵션은
:guilabel:`환경설정->네트워크 공유->고급`에 있습니다. 다만 주의해야 합니다.
특히 Firefox 같은 일부 브라우저는 임의의 웹사이트도 로컬 서버에 접근할 수 있도록 허용합니다. Firefox를 사용한다면, 아래에 설명된 것처럼 사용자 이름과 비밀번호를 설정하는 편이 더 좋습니다. 로컬 네트워크 접근 제한은
Firefox v149 기준으로도 아직 구현되지 않았습니다.

- 인터넷을 통한 접근을 허용하려면, 서버에 사용자 계정을 설정하고

calibredb`의 :option:–username` 및 --password 옵션을 사용하여 접근 권한을 부여해야 합니다. calibre-server`의 사용자 인증은 `–enable-auth`` 옵션을 사용하고 ``–manage-users``로 사용자 계정을 생성하여 설정할 수 있습니다.
메인 calibre 프로그램에서 서버를 실행하는 경우에는
:guilabel:`환경설정->네트워크 공유->사용자 이름/비밀번호 요구`를 사용하십시오.

실행 중인 콘텐츠 서버에 연결하려면 서버의 URL을 --with-library 옵션에 전달하십시오. 자세한 내용과 예시는 해당 옵션의 문서를 참조하십시오.

- 전역 옵션

- list

- add

- 폴더에서 추가

- remove

- add_format

- remove_format

- show_metadata

- set_metadata

- export

- catalog

- EPUB 옵션

- saved_searches

- add_custom_column

- custom_columns

- remove_custom_column

- set_custom

- restore_database

- check_library

- list_categories

- backup_metadata

- clone

- embed_metadata

- search

- fts_index

- fts_search

전역 옵션

--help, -h

이 도움말 메시지를 표시한 후 종료

--library-path, --with-library

calibre 라이브러리 경로입니다. 기본값은 설정에 저장된 경로를 사용하는 것입니다. 원격 라이브러리에서 작업을 수행하려면 calibre 콘텐츠 서버에 연결할 수도 있습니다. 이를 위해서는 http://hostname:port/#library_id 형식의 URL을 사용하십시오. 예: http://localhost:8080/#mylibrary. library_id는 콘텐츠 서버에서 연결하려는 라이브러리의 ID입니다. 특수한 library_id 값인 - 를 사용하면 서버에서 사용 가능한 라이브러리 ID 목록을 얻을 수 있습니다. 콘텐츠 서버를 통한 접근 설정 방법은 https://manual.calibre-ebook.com/ko/generated/ko/calibredb.html을 참조하십시오.

--password

calibre 콘텐츠 서버에 연결할 때 사용할 비밀번호입니다. 표준 입력에서 비밀번호를 읽으려면 특수 값 <stdin>을 사용하십시오. 파일에서 비밀번호를 읽으려면 <f:/path/to/file>을 사용하십시오(즉, <f: 다음에 파일의 전체 경로를 쓰고 마지막에 >를 붙입니다). 위의 꺾쇠괄호는 필수이므로 셸에서 이스케이프하거나 따옴표로 감싸야 합니다.

--timeout

네트워크를 통해 캘리버 라이브러리에 연결할 때의 제한 시간(초)입니다. 기본값은 2분입니다.

--username

캘리버 콘텐츠 서버에 연결할 사용자 이름

--version

프로그램의 버전을 표시한 후 종료합니다.

list

calibredb list [options]

캘리버 데이터베이스에 등록되어 있는 책들의 목록을 표시합니다.

calibredb에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

--ascending

오름차순의 결과로 정렬합니다

--fields, -f

데이터베이스의 책 목록을 표시할 때 보여줄 필드입니다. 필드는 쉼표로 구분해 지정해야 합니다. 사용 가능한 필드: author_sort, authors, comments, cover, formats, identifiers, isbn, languages, last_modified, pubdate, publisher, rating, series, series_index, size, tags, template, timestamp, title, uuid 기본값: title,authors. 특수 필드 "all"을 사용하면 모든 필드를 선택할 수 있습니다. 위의 내장 필드 외에도 사용자 정의 필드는 *field_name 형식으로 사용할 수 있습니다. 예를 들어 사용자 정의 필드 #rating은 *rating을 사용하십시오

--for-machine

기계로 처리하기 더 적합한 JSON 형식으로 출력합니다. 이 경우 줄 너비와 구분자 옵션은 무시됩니다.

--limit

표시할 최대 결과 수입니다. 기본값: 전체

--line-width, -w

출력에서 한 줄의 최대 너비입니다. 기본값은 화면 크기 자동 감지입니다.

--prefix

모든 파일 경로의 접두사입니다. 기본값은 라이브러리 폴더의 절대 경로입니다.

--search, -s

검색 쿼리로 결과를 필터링합니다. 검색 쿼리 의 형식은 사용자 설명서에서 검색 관련 설명서를 참조하십시오. 기본값은 필터링을 하지 않는 것입니다.

--separator

문자열은 필드를 구분하는데 사용됩니다. 기본값은 공백입니다.

--sort-by

결과를 정렬할 필드입니다. 쉼표로 구분해 여러 필드를 지정할 수 있습니다. 사용 가능한 필드: author_sort, authors, comments, cover, formats, identifiers, isbn, languages, last_modified, pubdate, publisher, rating, series, series_index, size, tags, template, timestamp, title, uuid 기본값: id. 위의 내장 필드 외에도 사용자 정의 필드는 *field_name 형식으로 사용할 수 있습니다. 예를 들어 사용자 정의 필드 #rating은 *rating을 사용하십시오

--template

필드 목록에 "template"가 있을 때 실행할 템플릿입니다. calibre 서버에 연결 중일 때는 템플릿이 무시된다는 점에 유의하십시오. 기본값: None

--template_file, -t

"template"가 필드 목록에 있을 때 실행할 템플릿이 들어 있는 파일 경로입니다. 기본값: 없음

--template_heading

템플릿 열의 제목입니다. 기본값: template. 옵션 :option:`–for-machine`이(가) 설정되어 있으면 이 옵션은 무시됩니다

add

calibredb add [옵션] 파일1 파일2 파일3 ...

지정된 파일을 데이터베이스에 책으로 추가합니다. 폴더를 지정할 수도 있습니다.
아래의 폴더 관련 옵션을 참조하십시오.

calibredb에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

--authors, -a

추가된 책(들)의 저자를 입력하기

--automerge, -m

비슷한 제목과 저자를 가진 책이 발견되면 들어오는 형식(파일)을 기존 책 레코드에 자동으로 병합합니다. "ignore" 값은 중복 형식을 버린다는 뜻입니다. "overwrite" 값은 라이브러리의 중복 형식을 새로 추가한 파일로 덮어쓴다는 뜻입니다. "new_record" 값은 중복 형식을 새 책 레코드에 넣는다는 뜻입니다.

--cover, -c

추가된 책에 사용할 표지의 경로

--duplicates, -d

--automerge 옵션이 우선 적용된다는 점에 유의하십시오. 이미 존재하는 책이라도 데이터베이스에 추가합니다. 비교는 책 제목과 저자를 기준으로 수행됩니다.

--empty, -e

비어있는 책 추가하기 (형식이 없는 책)

--identifier, -I

이 책의 식별자를 설정하십시오. 예: -I asin:XXX -I isbn:YYY

--isbn, -i

추가된 책(들)의 ISBN을 입력하기

--languages, -l

쉼표로 구분된 언어 목록 (일부 언어 이름도 인식될 수 있지만, ISO639 언어 코드를 사용하는 것이 가장 좋습니다)

--series, -s

추가된 책(들)의 시리즈를 입력하기

--series-index, -S

추가된 책(들)의 일련번호을 입력하기

--tags, -T

추가된 책(들)의 태그를 입력하기

--title, -t

추가된 책(들)의 이름을 입력하기

폴더에서 추가

폴더에서 책을 추가하는 동작을 제어하는 옵션입니다. 기본적으로 알려진 전자책 파일 형식 확장자를 가진 파일만 추가됩니다.

--add

파일명(글롭) 패턴입니다. 폴더에서 파일을 검색할 때 이 패턴과 일치하는 파일은 알려진 전자책 파일 형식이 아니어도 추가됩니다. 여러 패턴을 지정하려면 이 옵션을 여러 번 지정할 수 있습니다.

--ignore

파일명(글롭) 패턴입니다. 폴더에서 파일을 검색할 때 이 패턴과 일치하는 파일은 무시됩니다. 여러 패턴을 지정하려면 이 옵션을 여러 번 지정할 수 있습니다. 예: *.pdf 는 모든 PDF 파일을 무시합니다

--one-book-per-directory, -1

각 폴더에 논리적으로 하나의 책만 들어 있으며, 그 안의 모든 파일은 그 책의 서로 다른 전자책 형식이라고 가정합니다

--recurse, -r

폴더를 재귀적으로 처리

remove

calibredb remove ids

ids로 식별되는 책을 데이터베이스에서 제거합니다. ids는 ID 번호의 쉼표 구분 목록이어야 합니다(ID 번호는 search 명령으로 확인할 수 있습니다). 예: 23,34,57-85(범위를 지정할 때 범위의 마지막 숫자는 포함되지 않습니다).

calibredb에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

--permanent

휴지통 사용 안 함

add_format

calibredb add_format [options] id ebook_file

ebook_file의 전자책을 id로 식별되는 논리적 책의 사용 가능한 형식에 추가합니다. id는 search 명령으로 확인할 수 있습니다. 형식이 이미 존재하면 바꾸지 않기 옵션이 지정된 경우를 제외하고 대체됩니다.

calibredb에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

--as-extra-data-file

이 파일을 전자책 형식이 아니라 책의 추가 데이터 파일로 추가

--dont-replace

이미 존재하는 형식이면 바꾸지 마세요

remove_format

calibredb remove_format [options] id fmt

id로 식별되는 논리적 책에서 fmt 형식을 제거합니다. id는 search 명령으로 확인할 수 있습니다. fmt는 LRF, TXT, EPUB 같은 파일 확장자여야 합니다. 논리적 책에 fmt 형식이 없으면 아무 작업도 하지 않습니다.

calibredb에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

show_metadata

calibredb show_metadata [options] id

id로 식별되는 책의 calibre 데이터베이스에 저장된 메타데이터를 표시합니다.
id는 search 명령에서 얻는 ID 번호입니다.

calibredb에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

--as-opf

OPF 양식(XML)에서 메타데이터를 인쇄합니다

set_metadata

calibredb set_metadata [options] book_id [/path/to/metadata.opf]

book_id로 식별되는 책의 calibre 데이터베이스 메타데이터를 OPF 파일 metadata.opf의 내용으로 설정합니다. book_id는 search 명령에서 얻는 책 ID 번호입니다. OPF 형식은 show_metadata 명령에 –as-opf 스위치를 사용해 빠르게 확인할 수 있습니다. –field 옵션을 사용해 개별 필드의 메타데이터를 설정할 수도 있습니다. –field 옵션을 사용하면 OPF 파일을 지정할 필요가 없습니다.

calibredb에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

--field, -f

설정할 필드입니다. 형식은 field_name:value입니다(예: --field tags:tag1,tag2). --list-fields`을 사용하여 모든 필드 이름 목록을 가져옵니다. 이 옵션을 여러 번 지정하여 여러 필드를 설정할 수 있습니다. 참고: 언어의 경우 ISO639 언어 코드를 사용해야 합니다(예: 영어는 en, 프랑스어는 fr 등). 식별자의 경우 구문은 :option:–field` identifiers:isbn:XXXX,doi:YYYYY입니다. 불리언(예/아니요) 필드의 경우 참/거짓 또는 예/아니요를 사용합니다.

--list-fields, -l

--field 옵션과 함께 사용할 수 있는 메타데이터 필드 이름 나열

export

calibredb export [options] ids

ids(쉼표로 구분된 목록)로 지정한 책을 파일 시스템으로 내보냅니다.
내보내기 작업은 책의 모든 형식, 표지, 메타데이터(OPF 파일)를 저장합니다. 책에 연결된 추가 데이터 파일도 함께 저장됩니다.
id 번호는 search 명령으로 확인할 수 있습니다.

calibredb에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

--all

데이터베이스의 모든 책을 내보냅니다. ID 목록은 무시합니다.

--dont-asciiize

파일 이름에 있는 모든 비영어 문자를 영어 대응 문자로 변환하도록 calibre에 지시합니다. 유니코드 파일명을 완전히 지원하지 않는 구형 파일 시스템에 저장할 때 유용합니다. 이 스위치를 지정하면 이 동작이 꺼집니다.

--dont-save-cover

보통, 캘리버는 표지를 실제 전자책 파일과 함께 별도의 파일에 저장합니다. 이 스위치를 지정하면 이 동작이 꺼집니다.

--dont-save-extra-files

책을 저장할 때 책과 연결된 데이터 파일도 함께 저장 이 스위치를 지정하면 이 동작이 꺼집니다.

--dont-update-metadata

캘리버가 캘리버 라이브러리에 있는 메타데이터를 가져와 저장된 파일의 메타데이터를 업데이트합니다. 디스크에 저장하는 시간이 더 걸리게 됩니다. 이 스위치를 지정하면 이 동작이 꺼집니다.

--dont-write-opf

보통, 캘리버는 실제 전자책 파일과 함께 분리된 OPF 파일 내에 메타데이터를 기록합니다. 이 스위치를 지정하면 이 동작이 꺼집니다.

--formats

책마다 저장할 형식의 목록이며 쉼표로 구분합니다. 기본값으로 모든(all) 이용가능한 형식으로 저장됩니다.

--progress

진행 상황 보고

--replace-whitespace

공백 문자를 밑줄로 교체합니다.

--single-dir

모든 책을 하나의 폴더로 내보내기

--template

저장되는 파일의 파일명과 폴더 구조를 제어하는 템플릿입니다. 기본값은 "{author_sort}/{title}/{title} - {authors}"이며 제목과 저자가 포함된 파일명을 사용해 저자별 하위 폴더에 책을 저장합니다. 사용할 수 있는 제어 항목은 다음과 같습니다: {author_sort, authors, id, isbn, languages, last_modified, pages, pubdate, publisher, rating, series, series_index, tags, timestamp, title}

--timefmt

날짜를 표시하는 양식. %d - 일, %b - 월, %m - 월(숫자), %Y - 년도. 기본값: %b, %Y

--to-dir

책을 지정한 폴더로 내보냅니다. 기본값: .

--to-lowercase

경로를 소문자로 변환합니다.

catalog

calibredb catalog /path/to/destination.(csv|epub|mobi|xml...) [옵션]

경로/대상 확장으로 지정된 형식으로 범주를 내보냅니다.
옵션은 생성된 범주 출력에 항목이 표시되는 방식을 제어합니다.
서로 다른 범주 형식은 서로 다른 옵션 집합을 지원합니다.
다른 옵션을 보려면 출력 파일 이름을 지정한 다음 –help 옵션을 지정하십시오.

calibredb에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

--ids, -i

범주에 추가할 데이터베이스 ID의 쉼표로 구분된 목록입니다. 선언되면, :option:`–search`는 무시됩니다. 기본값: 모두

--search, -s

검색 쿼리로 결과를 필터링합니다. 검색 쿼리 의 형식은 사용자 설명서에서 검색 관련 문서를 참조하십시오. 기본값: 필터링 없음

--verbose, -v

상세한 출력 정보를 표시합니다. 디버깅 시에 유용합니다.

EPUB 옵션

--catalog-title

메타데이터에서 제목으로 사용되는 생성된 범주의 제목입니다. 기본값: '기본값(%d)' 적용 대상: AZW3, EPUB, MOBI 출력 형식

--cross-reference-authors

여러 저자가 있는 책의 저자 섹션에서 상호 참조를 만듭니다. 기본값: '기본값(%d)' 적용 대상: AZW3, EPUB, MOBI 출력 형식

--debug-pipeline

변환 파이프라인의 다른 단계에서 출력을 지정된 폴더에 저장합니다. 변환 프로세스의 어느 단계에서 버그가 발생하는지 확실하지 않은 경우 유용합니다. 기본값: '기본값(%d)' 적용 대상: AZW3, EPUB, MOBI 출력 형식

--exclude-genre

장르로 제외할 태그를 설명하는 정규식입니다. 기본값: '기본값(%d)'은 괄호로 묶인 태그를 제외합니다(예: '[Project Gutenberg]', 및 '+', 책 읽기의 기본 태그. 적용 대상: AZW3, EPUB, MOBI 출력 형식

--exclusion-rules

생성된 범주에서 책을 제외하는 데 사용되는 규칙을 지정합니다. 제외 규칙의 모델은 다음 중 하나입니다. ('<rule name>','태그','<comma-separated list of tags>') 또는 ('<rule name>','<custom column>','<pattern>'). 예를 들어: (('보관된 책','#상태','보관됨'),) 맞춤 열 '상태'에서 값이 '보관됨'인 책은 제외됩니다. 여러 규칙이 정의되면 모든 규칙이 적용됩니다. 기본값: "기본값(%d)" 적용 대상: AZW3, EPUB, MOBI 출력 형식

--generate-authors

'저자' 섹션을 범주에 포함합니다. 기본값: '기본값(%d)' 적용 대상: AZW3, EPUB, MOBI 출력 형식

--generate-descriptions

'책 설명' 섹션을 범주에 포함합니다. 기본값: '기본값(%d)' 적용 대상: AZW3, EPUB, MOBI 출력 형식

--generate-genres

'장르' 섹션을 범주에 포함합니다. 기본값: '기본값(%d)' 적용 대상: AZW3, EPUB, MOBI 출력 형식

--generate-recently-added

'최근 추가된 항목' 섹션을 범주에 포함합니다. 기본값: '기본값(%d)' 적용 대상: AZW3, EPUB, MOBI 출력 형식

--generate-series

'시리즈' 섹션을 범주에 포함합니다. 기본값: '기본값(%d)' 적용 대상: AZW3, EPUB, MOBI 출력 형식

--generate-titles

'제목' 섹션을 범주에 포함합니다. 기본값: '기본값(%d)' 적용 대상: AZW3, EPUB, MOBI 출력 형식

--genre-source-field

'장르' 섹션의 소스 필드입니다. 기본값: '기본값(%d)' 적용 대상: AZW3, EPUB, MOBI 출력 형식

--header-note-source-field

설명 헤더에 삽입할 메모 텍스트가 포함된 사용자 정의 필드입니다. 기본값: '기본값(%d)' 적용 대상: AZW3, EPUB, MOBI 출력 형식

--merge-comments-rule

#<custom field>:[이전|이후]:[참|거짓] 지정: <custom field> 책 설명과 병합할 메모가 포함된 사용자 정의 필드 [이전|이후] 책 설명에 대한 책 설명 배치 [참|거짓] - 메모와 책 설명사이에 가로줄을 삽입합니다. 기본값: '::' 적용 대상: AZW3, EPUB, MOBI 출력 형식

--output-profile

출력 프로필을 지정합니다. 경우에 따라 장치에 맞게 카탈로그를 최적화하려면 출력 프로필이 필요합니다. 예를 들어 'kindle' 또는 'kindle_dx'를 사용하면 섹션과 기사로 구성된 구조화된 목차가 만들어집니다. 기본값: 'None' 적용 대상: AZW3, EPUB, MOBI 출력 형식

--prefix-rules

읽은 책, 위시리스트 항목 및 기타 사용자 지정 접두사를 나타내는 접두사를 포함하는 데 사용되는 규칙을 지정합니다. 접두사 규칙의 모델은 ('<rule name>','<source field>','<pattern>','<prefix>')입니다. 여러 규칙이 정의된 경우 첫 번째 일치 규칙이 사용됩니다. 기본값: "기본값(%d)" 적용 대상: AZW3, EPUB, MOBI 출력 형식

--preset

GUI 범주 빌더로 작성된 명명된 프리셋을 사용합니다. 프리셋은 범주 작성을 위한 모든 설정을 지정합니다. 기본값: '기본값(%d)' 적용 대상: AZW3, EPUB, MOBI 출력 형식

--thumb-width

범주의 책 표지에 대한 힌트 크기(인치)입니다. 범위: 1.0 - 2.0 기본값: '기본값(%d)' 적용 대상: AZW3, EPUB, MOBI 출력 형식

--use-existing-cover

범주 생성 시 기존 표지를 교체합니다. 기본값: '기본값(%d)' 적용 대상: AZW3, EPUB, MOBI 출력 형식

saved_searches

calibredb saved_searches [옵션] (list|add|remove)

이 데이터베이스에 저장된 저장된 검색을 관리합니다.
이미 존재하는 이름으로 쿼리를 추가하려고 하면 대체됩니다.

추가할때 구문:

calibredb saved_searches add search_name search_expression

제거할때 구문:

calibredb saved_searches remove search_name
calibredb에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

add_custom_column

calibredb add_custom_column [options] label name datatype

사용자 정의 열을 만듭니다. label은 열의 내부용 이름입니다.
공백이나 콜론을 포함하면 안 됩니다. name은 사용자가 보는 열 이름입니다.
datatype은 다음 중 하나입니다: bool, comments, composite, datetime, enumeration, float, int, rating, series, text
calibredb에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

--display

이 열의 데이터가 해석되는 방식을 사용자 지정하기 위한 옵션 사전입니다. 이것은 JSON 문자열입니다. 열거 열의 경우 --display"{\ "enum_values\ ":[\ "val1\ ", \ "val2\ "]}"를 사용합니다. 표시 변수에 들어갈 수 있는 많은 옵션이 있습니다. 열 유형별 옵션은 다음과 같습니다: 복합: composite_template, composite_sort, make_category,contains_html, use_decorations 날짜시간: date_format 열거: enum_values, enum_colors, use_decorations: enum_values, enum_colors, use_decorations int, float: number_format 텍스트: is_names, use_decorations 합법적인 조합을 찾는 가장 좋은 방법은 GUI에서 적절한 유형의 사용자 지정 열을 만든 다음 책에 대한 백업 OPF를 확인하는 것입니다(열이 추가된 이후 새 OPF가 생성되었는지 확인). OPF의 새 열에 대한 "디스플레이"에 대한 JSON이 표시됩니다.

--is-multiple

이 열은 태그와 유사한 데이터(즉, 쉼표로 구분된 여러 값)를 저장합니다. datatype이 text일 때만 적용됩니다.

custom_columns

calibredb custom_columns [옵션]

사용 가능한 사용자 지정 열을 나열합니다. 열 레이블과 ID를 표시합니다.

calibredb에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

--details, -d

각 열에 대한 상세 정보를 표시합니다.

remove_custom_column

calibredb remove_custom_column [options] label

label로 식별되는 사용자 정의 열을 제거합니다. 사용 가능한 열은 custom_columns 명령으로 확인할 수 있습니다.

calibredb에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

--force, -f

확인을 다시 묻지 않음

set_custom

calibredb set_custom [options] column id value

id로 식별되는 책의 사용자 정의 열 값 설정.
ID 목록은 search 명령으로 확인할 수 있습니다.
사용자 정의 열 이름 목록은 custom_columns 명령으로 확인할 수 있습니다.

calibredb에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

--append, -a

열이 여러 값을 저장하는 경우 기존 값을 대체하지 않고 지정한 값을 덧붙입니다.

restore_database

calibredb restore_database [options]

calibre 라이브러리의 각 폴더에 있는 OPF 파일에 저장된 메타데이터에서 이 데이터베이스를 복원합니다. metadata.db 파일이 손상되었을 때 유용합니다.

경고: 이 명령은 데이터베이스를 완전히 다시 생성합니다. 저장된 검색, 사용자 범주, 플러그보드, 책별 변환 설정, 사용자 정의 레시피를 모두 잃게 됩니다. 복원된 메타데이터의 정확도는 OPF 파일에 들어 있는 내용에만 의존합니다.

calibredb에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

--really-do-it, -r

실제로 복구를 수행합니다. 이 옵션을 지정하지 않으면 명령이 실행되지 않습니다.

check_library

calibredb check_library [options]

라이브러리를 나타내는 파일 시스템에 대해 몇 가지 검사를 수행합니다. 보고서는 invalid_titles, extra_titles, invalid_authors, extra_authors, missing_formats, extra_formats, extra_files, missing_covers, extra_covers, malformed_formats, malformed_paths, failed_folders입니다.

calibredb에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

--csv, -c

CSV로 출력

--ignore_extensions, -e

무시할 확장자의 쉼표로 구분된 목록. 기본값: 전체

--ignore_names, -n

무시할 이름의 쉼표로 구분된 목록. 기본값: 전체

--report, -r

쉼표로 구분된 보고서 목록입니다. 기본값: all

--vacuum-fts-db

전체 텍스트 검색 데이터베이스를 vacuum 합니다. 데이터베이스 크기에 따라 매우 느리고 메모리를 많이 사용할 수 있습니다.

list_categories

calibredb list_categories [options]

데이터베이스의 범주 정보에 대한 보고서를 생성합니다.
이 정보는 태그 브라우저에 표시되는 내용과 같습니다.

calibredb에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

--categories, -r

범주 조회 이름의 쉼표로 구분된 목록입니다. 기본값: 모두

--csv, -c

CSV로 출력

--dialect

생성할 CSV 파일 유형입니다. 선택 가능 값: excel, excel-tab, unix

--item_count, -i

범주 내 각 항목의 개수 대신 범주의 항목 수만 출력

--width, -w

출력에서 한 줄의 최대 너비입니다. 기본값은 화면 크기 자동 감지입니다.

backup_metadata

calibredb backup_metadata [options]

데이터베이스에 저장된 메타데이터를 각 책 폴더의 개별 OPF 파일로 백업합니다. 보통은 자동으로 수행되지만, –all 옵션을 사용하면 이 명령으로 OPF 파일을 강제로 다시 생성할 수 있습니다.

일반적으로는 이렇게 할 필요가 없습니다. 메타데이터가 변경될 때마다 OPF 파일이 자동으로 백업되기 때문입니다.

calibredb에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

--all

기본적으로 이 명령은 오래된 OPF 파일을 가진 책에만 동작합니다. 이 옵션을 사용하면 모든 책에 대해 동작합니다.

clone

calibredb clone path/to/new/library

현재 라이브러리의 복제본을 만듭니다. 이 명령은 현재 라이브러리와 동일한 사용자 정의 열, 가상 라이브러리 및 기타 설정을 가진 새 빈 라이브러리를 만듭니다.

복제된 라이브러리에는 책이 들어 있지 않습니다. 책까지 포함한 전체 복사본을 만들고 싶다면 파일 시스템 도구를 사용해 라이브러리 폴더를 그대로 복사하십시오.

calibredb에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

embed_metadata

calibredb embed_metadata [options] book_id

calibre 데이터베이스의 메타데이터를 calibre 라이브러리에 저장된 실제 책 파일에 반영합니다. 일반적으로 메타데이터는 calibre에서 파일을 내보낼 때만 갱신되므로, 파일 자체를 제자리에서 갱신하고 싶을 때 이 명령이 유용합니다. 파일 형식마다 지원하는 메타데이터의 양이 다르다는 점에 유의하십시오. 모든 책의 메타데이터를 갱신하려면 book_id에 특수 값 ‘all’을 사용할 수 있습니다. 공백으로 구분한 여러 책 ID와 하이픈으로 구분한 ID 범위도 지정할 수 있습니다. 예: calibredb embed_metadata 1 2 10-15 23
calibredb에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

--only-formats, -f

지정한 형식의 파일에 대해서만 메타데이터를 갱신합니다. 여러 형식을 지정하려면 이 옵션을 여러 번 사용하십시오. 기본적으로는 모든 형식이 갱신됩니다.

search

calibredb search [옵션] search expression

지정된 검색어에 대해 라이브러리를 검색하여 검색 표현식과 일치하는 책 ID의 쉼표로 구분된 목록을 반환합니다. 출력 형식은 ID 목록을 입력으로 허용하는 다른 명령에 입력하는 데 유용합니다.

검색 표현식은 캘리버의 강력한 검색어 언어의 모든 것이 될 수 있습니다. 예: calibredb search author:asimov ‘title:”i robot”’

calibredb에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

--limit, -l

리턴할 결과의 최대 갯수. 기본값은 모든 결과입니다.

fts_index

calibredb fts_index [options] enable/disable/status/reindex

전체 텍스트 검색 색인 처리 과정을 제어합니다.

enable

이 라이브러리에서 FTS 색인을 켭니다

disable

이 라이브러리에서 FTS 색인을 끕니다

status

현재 색인 상태를 표시합니다

reindex

특정 책 또는 전체 라이브러리를 다시 색인할 때 사용합니다.
특정 책만 다시 색인하려면 reindex 명령 뒤에 추가 인수로 책 ID를 지정하십시오. 책 ID를 지정하지 않으면 전체 라이브러리를 다시 색인합니다.

calibredb에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

--indexing-speed

색인 속도입니다. fast는 컴퓨터 자원을 모두 사용해 빠르게 색인하고, slow는 자원을 덜 쓰는 색인입니다. 이 속도 설정은 매번 실행 후 다시 slow로 초기화됩니다.

--wait-for-completion

모든 책이 인덱싱될 때까지 주기적으로 진행 상황을 표시하며 대기

fts_search

calibredb fts_search [옵션] 검색 표현식

전체 라이브러리 또는 그 일부에서 전체 텍스트 검색을 수행합니다.

calibredb에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

--do-not-match-on-related-words

관련 단어가 아닌 정확한 단어만 일치시킵니다. 따라서 correction은 correcting과 일치하지 않습니다.

--include-snippets

각 일치 항목 주변의 텍스트 스니펫을 포함합니다. 참고로, 이 기능은 검색 속도를 훨씬 느리게 만듭니다.

--indexing-threshold

검색을 허용하기 전에 인덱싱되어야 하는 라이브러리 비율입니다. 기본값은 90입니다.

--match-end-marker

스니펫 내에서 일치하는 단어의 끝을 나타내는 데 사용되는 마커입니다

--match-start-marker

스니펫 내에서 일치하는 단어의 시작을 나타내는 데 사용되는 마커입니다

--output-format

검색 결과를 출력할 형식입니다. 일반 텍스트는 "text", JSON 출력은 "json"을 사용합니다.

--restrict-to

검색할 책을 검색식 또는 ID로 제한합니다. 예: IDs로 제한하려면 ids:1,2,3, 태그 foo가 있는 책으로 제한하려면 search:tag:foo

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 명령행 인터페이스 »
- ebook-convert

ebook-convert

ebook-convert input_file output_file [options]

전자책을 한 형식에서 다른 형식으로 변환합니다.

input_file은 입력 파일이고 output_file은 출력 파일입니다. 둘 다 명령의 첫 두 인수로 지정해야 합니다.

출력 전자책 형식은 output_file의 파일 확장자로 추정합니다. output_file은 출력 파일 확장자가 EXT인 특수 형식 .EXT 로도 지정할 수 있습니다. 이 경우 출력 파일 이름은 입력 파일 이름에서 파생됩니다. 파일 이름은 하이픈으로 시작하면 안 됩니다. 마지막으로 output_file에 확장자가 없으면 폴더로 간주하며, HTML 파일들로 이루어진 “오픈 전자책”(OEB)을 그 폴더에 씁니다. 이 파일들은 원래 출력 플러그인에 전달되었을 파일들입니다.

입력 파일과 출력 파일을 지정한 뒤에는 여러 옵션을 지정해 변환을 사용자 지정할 수 있습니다. 사용 가능한 옵션은 입력 및 출력 파일 형식에 따라 달라집니다. 해당 옵션의 도움말을 보려면 입력 파일과 출력 파일을 지정한 다음 -h 옵션을 사용하십시오.

변환 시스템의 전체 문서는 다음을 참조하십시오. 전자책 변환

:command:`ebook-convert`에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

macOS가 장치 기본 메모리의 파일에 접근하려다 권한이 거부되었습니다. 시스템 설정 > 개인정보 보호 및 보안 > 개인정보 보호 > 파일 및 폴더에서 명시적으로 권한을 부여해야 합니다. 내부 오류: ebook-convert myfile.input_format myfile.output_format -h

- 입력 옵션

- 출력 옵션

- 모양새

- 합리적 추측에 의한(Heuristic) 처리

- 검색 및 교체

- 구조 탐지

- 목차

- 메타데이터

- 디버그

--help, -h

이 도움말 메시지를 표시한 후 종료

--input-profile

입력 프로필을 지정합니다. 입력 프로필은 입력 문서의 여러 정보를 변환 시스템이 어떻게 해석할지 알려 줍니다. 예를 들어 해상도에 따라 달라지는 길이(즉, 픽셀 단위 길이) 같은 정보입니다. 선택 가능한 값: cybookg3, cybook_opus, default, hanlinv3, hanlinv5, illiad, irexdr1000, irexdr800, kindle, msreader, mobipocket, nook, sony, sony300, sony900

--list-recipes

내장된 레시피 이름을 나열합니다. 다음과 같은 내장된 레시피에서 전자책을 만들 수 있습니다: ebook-convert "레시피 이름.recipe" 출력.epub

--output-profile

출력 프로필을 지정합니다. 출력 프로필은 생성된 문서를 지정한 장치에 맞게 어떻게 최적화할지 변환 시스템에 알려 줍니다. 경우에 따라 특정 장치에 맞춰 출력을 최적화할 수 있지만, 대체로 꼭 필요하지는 않습니다. 선택 가능한 값:cybookg3, cybook_opus, default, hanlinv3, hanlinv5, illiad, ipad, ipad3, irexdr1000, irexdr800, jetbook5, kindle, kindle_dx, kindle_fire, kindle_oasis, kindle_pw, kindle_pw3, kindle_scribe, kindle_voyage, kobo, msreader, mobipocket, nook, nook_color, nook_hd_plus, pocketbook_inkpad3, pocketbook_lux, pocketbook_hd, pocketbook_900, pocketbook_pro_912, galaxy, sony, sony300, sony900, sony-landscape, sonyt3, generic_eink, generic_eink_hd, generic_eink_large, tablet

--version

프로그램의 버전을 표시한 후 종료합니다.

입력 옵션
mobi 파일 입력의 처리를 제어하는 옵션입니다.

--input-encoding

입력 문서의 문자 인코딩을 지정합니다. 이 옵션이 설정되면 문서 자체에 선언된 인코딩은 무시됩니다. 인코딩 선언이 없거나 인코딩 선언에 오류가 있는 문서에서 특히 유용합니다.

출력 옵션
epub 출력의 처리를 제어하는 옵션입니다.

--dont-split-on-page-breaks

쪽 넘김에서 분할하기 기능을 끕니다. 보통은 입력 파일은 두 개의 파일 내의 쪽 넘김마다 자동으로 분할합니다. 이것은 출력하는 책에 더 빠르게 분석하고 적은 자원을 사용할 수 있게 합니다. 하지만, 분할하기는 느리며 원본 파일이 매우 큰 쪽 넘김 번호를 담고 있다면 쪽 넘김시 분할하기를 꺼야 합니다.

--epub-flatten

이 옵션은 EPUB을 FBReaderJ에서 사용하기 원하는 경우에만 필요합니다. 이 옵션은 EPUB 내부의 파일 시스템 구조를 단순화시켜, 모든 파일을 최상위 레벨에 위치시킵니다.

--epub-inline-toc

책 내용의 일부분으로 나타나게 될 인라인 목차를 삽입합니다.

--epub-max-image-size

최대 이미지 크기(너비 x 높이)입니다. 값 profile은 출력 프로필의 화면 크기를 사용한다는 뜻입니다. 값 none은 최대 크기를 지정하지 않는다는 뜻입니다. 예를 들어 값 100x200를 지정하면 모든 이미지의 너비가 100픽셀, 높이가 200픽셀을 넘지 않도록 조정됩니다. 이는 실제 이미지 파일 자체의 크기에만 영향을 줍니다. 문서에 적용된 스타일에 따라 렌더링 크기는 달라질 수 있습니다.

--epub-toc-at-end

삽입된 인라인 목차를 책의 첫 부분 대신 끝 부분에 넣습니다.

--epub-version

생성할 EPUB 파일의 버전입니다. EPUB 2가 가장 호환성이 높으므로, 실제로 필요하다는 것을 아는 경우에만 EPUB 3를 사용하십시오.

--extract-to

이미지에 사용되는 색 수를 줄입니다. PNG 출력 형식을 선택했을 때만 동작합니다. 파일 크기를 줄이는 데 유용합니다. 끄려면 0으로 설정하십시오. 최대값은 256이며 기본적으로 꺼져 있습니다.

--flow-size

이 크기(KB)보다 큰 모든 HTML 파일을 분할합니다. 대부분의 EPUB 리더는 큰 파일 크기를 처리하지 못하므로 필요합니다. 기본값 260KB 는 Adobe Digital Editions 에 필요한 크기입니다. 크기 기준 분할을 비활성화하려면 0으로 설정하십시오.

--no-default-epub-cover

보통은, 입력 파일에 표지가 없고 이 옵션을 선택하지 않을 경우, 기본 표지는 제목, 저자 등과 함께 생성됩니다. 이 옵션을 비활성화하면 표지를 생성하지 않습니다.

--no-svg-cover

책 표지에 SVG를 사용하지 않습니다. EPUB가 iPhone이나 JetBook Lite처럼 SVG를 지원하지 않는 장치에 사용할 예정이면 이 옵션을 사용하세요. 이 옵션을 사용하지 않으면, 앞에서 명시한 장치들은 표지를 빈 면으로 표시하게 됩니다.

--preserve-cover-aspect-ratio

SVG 표지를 사용할 때 이 옵션을 사용하면 표지가 사용 가능한 화면 영역을 덮도록 크기가 조정되지만 종횡비(너비 대 높이 비율)은 유지됩니다. 즉, 이미지의 측면 또는 상단과 하단에 흰색 테두리가 있을 수 있지만 이미지는 왜곡되지 않습니다. 이 옵션이 없으면 이미지가 약간 왜곡될 수 있지만 테두리는 없습니다.

--pretty-print

지정된 경우, 출력 플러그인은 사람이 읽을 수 있는 출력을 최대한 생성하려고 시도합니다. 일부 출력 플러그인에 대해서는 영향을 미치지 않을 수 있습니다.

--toc-title

생성된 인라인 목차의 제목

모양새

출력의 모양새를 제어하는 옵션입니다

--asciiize

유니코드 문자를 ASCII 표기로 변환합니다. 이 작업은 유니코드 문자를 ASCII 로 바꾸므로 주의해서 사용하십시오. 예를 들어 "Pelé" 는 "Pele" 로 바뀝니다. 또한 하나의 문자를 여러 방식으로 표기할 수 있는 경우(예: 중국어와 일본어가 공유하는 문자) 현재 calibre 인터페이스 언어를 기준으로 한 표기가 사용됩니다.

--base-font-size

기본 글꼴 크기(pts 단위)입니다. 생성된 책의 모든 글꼴 크기는 이 값을 기준으로 다시 조정됩니다. 값을 크게 하면 출력물의 글꼴이 커지고, 작게 하면 반대로 작아집니다. 기본값인 0이면 선택한 출력 프로필을 기준으로 기본 글꼴 크기가 정해집니다.

--change-justification

텍스트 자리 맞춤을 변경합니다. "왼쪽" 값은 원본의 모든 정렬된 텍스트를 왼쪽 정렬된(즉, 정렬되지 않은) 텍스트로 변환합니다. "양쪽 정렬" 값은 모든 정렬이 안된 본문을 양쪽 정렬하도록 변환합니다. "원본"(기본값) 값은 원본 파일의 자리 맞춤을 바꾸지 않습니다. 일부 출력 형식만 자리 맞춤을 지원합니다.

--disable-font-rescaling

글꼴 크기의 모든 크기 조정을 비활성화합니다.

--embed-all-fonts

입력 문서에서 참조되지만 아직 포함되지 않은 모든 글꼴을 포함합니다. 그러면 시스템에서 글꼴을 검색하고 발견하면 포함됩니다. 포함은 변환하려는 형식이 EPUB, AZW3, DOCX 또는 PDF와 같은 포함 글꼴을 지원하는 경우에만 작동합니다. 이 문서에 사용된 글꼴을 포함할 수 있는 적절한 라이선스가 있는지 확인하십시오.

--embed-font-family

지정된 글꼴 모음을 책에 포함합니다. 책에 사용되는 "기본" 글꼴을 지정합니다. 입력 문서가 자체 글꼴을 지정하는 경우 이 기본 글꼴을 재정의할 수 있습니다. 필터 스타일 정보 옵션을 사용하여 입력 문서에서 글꼴을 제거할 수 있습니다. 글꼴 포함은 주로 EPUB, AZW3 및 DOCX와 같은 일부 출력 형식에서만 작동합니다.

--expand-css

기본적으로 calibre는 margin, padding, border 같은 다양한 CSS 속성에 축약형을 사용합니다. 이 옵션을 사용하면 대신 완전히 펼친 형식을 사용합니다. Nook은 축약형 CSS를 처리하지 못하므로, 출력 프로필이 Nook 계열일 때 EPUB 파일을 생성하면 CSS는 항상 펼친 형식으로 저장됩니다.

--extra-css

CSS 스타일시트 또는 원시 CSS에 대한 경로입니다. 이 CSS는 소스 파일의 스타일 규칙에 추가되므로 해당 규칙을 재정의하는 데 사용할 수 있습니다.

--filter-css

CSS 스타일 규칙에서 제외시킬, 쉼표로 구분된, CSS 속성들 목록. 사용자의 장치에서 기존의 특정 스타일 정보를 덮어쓰는 것이 막혀져 있는 경우에 유용합니다. 예시: font-family, color, margin-left, margin-right

--font-size-mapping

CSS 글꼴 크기 이름을 pts 단위의 글꼴 크기에 매핑합니다. 예: 12,12,14,16,18,20,22,24. 이는 xx-small부터 xx-large까지의 크기에 대한 매핑이며, 마지막 값은 huge 글꼴에 해당합니다. 글꼴 재조정 알고리즘은 이 값들을 사용해 글꼴 크기를 적절히 다시 조정합니다. 기본값은 선택한 출력 프로필에 기반한 매핑입니다.

--insert-blank-line

문단 사이에 빈 줄을 삽입합니다. 원본 파일이 문단(<p>, <div> 태그)을 사용하지 않으면 작동하지 않습니다.

--insert-blank-line-size

삽입된 빈 줄의 높이를 (em 단위로) 지정합니다. 문단 사이의 줄 높이는 여기에 설정된 값의 두 배가 됩니다.

--keep-ligatures

입력 문서에 있는 합자(ligature)를 보존합니다. 합자는 ff, fi, fl처럼 두 문자가 결합된 문자입니다. 대부분의 리더는 기본 글꼴에서 합자를 지원하지 않으므로 제대로 렌더링되지 않을 가능성이 큽니다. 기본적으로 calibre는 합자를 해당하는 일반 문자 쌍으로 바꿉니다. 여기서의 합자는 CSS나 글꼴 스타일로 만든 합자가 아니라 유니코드 합자만을 의미합니다. 이 옵션을 사용하면 이를 그대로 보존합니다.

--line-height

pt 단위의 줄 높이입니다. 연속적인 텍스트 줄 간의 간격을 조절합니다. 자체 줄 높이를 정의하지 않은 요소에만 적용됩니다. 대부분의 경우, 최소 줄 높이 옵션이 더 유용합니다. 기본적으로 줄 높이 조작은 수행되지 않습니다.

--linearize-tables

몇몇 잘못 디자인된 문서들은 페이지 안의 텍스트 배치를 조정하기 위하여 표를 사용합니다. 이러한 문서들을 변환하였을 때, 때때로 텍스트가 페이지를 벗어나거나 다른 영역을 침범하기도 합니다. 이 옵션은 표 안의 내용들을 추출한 뒤 일직선으로 늘어 놓아져 있는 형태로 만듭니다.

--margin-bottom

하단 여백을 pt 단위로 지정합니다. 기본값은 5.0입니다. 0보다 작게 설정하면 여백이 아예 지정되지 않습니다(원본 문서의 여백 설정이 보존됨). 참고: PDF나 DOCX와 같은 페이지 기반 양식들은 자신들이 갖고 있는 여백 설정이 우선합니다.

--margin-left

왼쪽 여백을 pt 단위로 설정합니다. 기본값은 5.0입니다. 0보다 작게 설정하면 여백이 아예 지정되지 않습니다(원본 문서의 여백 설정이 보존됨). 참고: PDF나 DOCX와 같은 페이지 기반 양식들은 자신들이 갖고 있는 여백 설정이 우선합니다.

--margin-right

오른쪽 여백을 pt 단위로 설정합니다. 기본값은 5.0입니다. 0보다 작게 설정하면 여백이 아예 지정되지 않습니다(원본 문서의 여백 설정이 보존됨). 참고: PDF나 DOCX와 같은 페이지 기반 양식들은 자신들이 갖고 있는 여백 설정이 우선합니다.

--margin-top

상단 여백을 pt 단위로 설정합니다. 기본값은 5.0입니다. 0보다 작게 설정하면 여백이 아예 지정되지 않습니다(원본 문서의 여백 설정이 보존됨). 참고: PDF나 DOCX와 같은 페이지 기반 양식들은 자신들이 갖고 있는 여백 설정이 우선합니다.

--minimum-line-height

해당 요소의 계산된 글꼴 크기에 대한 백분율로 표시되는 최소 줄 높이입니다. 캘리버는 입력 문서가 지정하는 것과 상관없이 모든 요소가 최소한 이 설정의 줄 높이를 갖도록 합니다. 비활성화하려면 0으로 설정하십시오. 기본값은 120%입니다. 현재 수행 중인 작업을 모르는 경우가 아니면 직접 라인 높이 사양에 대해 우선적으로 이 설정을 사용하십시오. 예를 들어, 이것을 240으로 설정하여 "이중 간격" 텍스트를 얻을 수 있습니다.

--remove-paragraph-spacing

문단 사이에 공백을 지웁니다. 또한 문단에 1.5em의 들여쓰기를 지정합니다. 공백 지우기는 원본 파일이 문단(<p>, <div> 태그)을 사용하지 않으면 작동하지 않습니다.

--remove-paragraph-spacing-indent-size

캘리버가 문단 사이의 빈 줄들을 제거할 때, 문단끼리 쉽게 구분되도록 하기 위하여 자동으로 단락에 들여쓰기를 지정합니다. 이 옵션은 그 들여쓰기의 너비를 (em 단위로) 조정합니다. 이 값을 음수로 지정하게 되면, 입력 문서에서 지정된 들여쓰기가 사용됩니다(즉, 캘리버가 들여쓰기를 변경하지 않음).

--smarten-punctuation

일반 따옴표, 대시, 말줄임표를 올바른 조판용 문자로 변환합니다. 자세한 내용: https://daringfireball.net/projects/smartypants.

--subset-embedded-fonts

모든 내장된 글꼴(embedded font)에 대하여 실제 사용된 문자만 취합니다. 내장된 모든 글꼴은 이 문서에서 사용된 문자들만 포함하도록 글꼴 파일 크기가 줄어들게 됩니다. 사용되지 않은 문자가 많으면서 파일의 크기가 큰 글꼴을 내장하는 경우 특히 유용합니다.

--transform-css-rules

이 책의 CSS 스타일을 변환하는 규칙이 포함된 파일의 경로입니다. 이러한 파일을 만드는 가장 쉬운 방법은 Calibre GUI에서 규칙 생성 마법사를 사용하는 것입니다. 변환 대화 상자의 "디자인 및 느낌 -> 스타일 변환" 섹션에서 마법사에 액세스할 수 있습니다. 규칙을 생성한 후에는 "내보내기" 버튼을 사용하여 파일에 저장할 수 있습니다.

--transform-html-rules

이 책의 HTML을 변환하는 규칙이 포함된 파일의 경로입니다. 이러한 파일을 만드는 가장 쉬운 방법은 Calibre GUI에서 규칙 생성 마법사를 사용하는 것입니다. 변환 대화 상자의 "디자인->HTML 변환" 섹션에서 마법사에 액세스할 수 있습니다. 규칙을 생성한 후에는 "내보내기" 버튼을 사용하여 파일에 저장할 수 있습니다.

--unsmarten-punctuation

꾸밈 따옴표, 대시, 생략부호들을 그것들의 평이한 형태로 대체하여 변환합니다.

합리적 추측에 의한(Heuristic) 처리

문서의 텍스트와 구조를 일반 패턴을 사용하여 변경합니다. 기본값으로 꺼져있습니다. 활성화하려면 –enable-heuristics 를 사용하세요. 개별 동작은 –disable-* 를 사용하여 비활성화할 수 있습니다.

--disable-dehyphenate

문서 전반에 걸쳐 하이픈 처리된 단어들을 분석합니다. 하이픈이 유지되어야 할지 제거되어야 할지를 결정하기 위한 사전으로 문서 그 자체를 사용합니다.

--disable-delete-blank-paragraphs

다른 단락들 사이에 빈 단락이 있는 경우 문서에서 이 빈 단락들을 삭제합니다

--disable-fix-indents

여러 개의 공란으로 처리된 들여쓰기를 CSS 들여쓰기로 바꾸어 처리합니다.

--disable-format-scene-breaks

왼쪽 정렬된 장면 구분 표식을 중앙 정렬합니다. 여러 행의 빈 줄을 사용한 약식의 장면 구분 표시를 가로 구분선으로 대체합니다.

--disable-italicize-common-cases

기울임체를 나타내는 패턴과 일반 용어를 찾아 기울임체 처리를 합니다.

--disable-markup-chapter-headings

형식이 지정되지 않은 장(章) 제목과 부제목을 감지합니다. h2 및 h3 태그로 변경합니다. 이 설정은 목차를 만들지 않지만 구조 감지 옵션과 함께 사용하여 목차를 만들 수 있습니다.

--disable-renumber-headings

연달아 있는 <h1> 또는 <h2> 태그를 찾습니다. 이러한 태그들은 장 제목의 중간에서 분리되는 것을 막기 위하여 다시 번호 매겨집니다.

--disable-unwrap-lines

구두점과 다른 형식요소들을 근거로 줄바꿈을 합칩니다.

--enable-heuristics

합리적 추측에 의한 처리를 활성화합니다. 어떤 합리적 추측에 의한 처리가 발생하려면 반드시 이 옵션이 켜져 있어야 합니다.

--html-unwrap-factor

행 길이의 어느 지점에서 줄바꿈을 합쳐야 할지 결정하는데 사용되는 스케일. 유요한 값은 0과 1사이의 십진수 값입니다. 기본값은 0.4이며, 행 길의 중간에서 약간 아래입니다. 문서에서 단지 몇몇 줄만 한줄로 합치기가 필요한 경우라면 이 값이 더 작아져야 합니다

--replace-scene-breaks

장면 구분 표시를 지정된 텍스트로 교체합니다. 기본값으로, 입력 문서에 사용된 텍스트가 사용됩니다.

검색 및 교체

문서의 텍스트와 구조를 사용자 정의 패턴을 사용하여 변경합니다.

--search-replace

검색 및 바꾸기 정규식을 담은 파일의 경로입니다. 파일에는 정규식 줄과 치환 패턴 줄이 번갈아 있어야 하며(치환 패턴 줄은 빈 줄일 수 있음), 정규식은 Python 정규식 문법이어야 하고 파일은 UTF-8로 인코딩되어 있어야 합니다.

--sr1-replace

sr1-search로 찾은 텍스트를 바꾸려면 교체를 선택합니다.

--sr1-search

sr1-replace로 교체할 검색 패턴(정규식)입니다.

--sr2-replace

sr2-search로 찾은 텍스트를 바꾸려면 교체를 선택합니다.

--sr2-search

sr2-replace로 바꿀 검색 패턴(정규식)입니다.

--sr3-replace

sr3-search로 찾은 텍스트를 바꾸려면 교체를 선택합니다.

--sr3-search

sr3-replace로 바꿀 검색 패턴(정규식)입니다.

구조 탐지

문서 구조의 자동 검출을 제어합니다.

--add-alt-text-to-img

출력에서 글꼴 색상을 제거하지 않습니다. 이는 TXT 출력 서식이 textile로 설정된 경우에만 유용합니다. Textile은 글꼴 색상 설정을 지원하는 유일한 서식입니다. 이 옵션을 지정하지 않으면 글꼴 색상은 설정되지 않으며 리더가 표시하는 기본 색상(일반적으로 검정색)을 사용합니다.

--chapter

장(章)제목을 탐지하기 위한 XPath 표현식. 기본값은 "chapter", "book", "section", "prologue", "epilogue", 또는 "part"란 단어를 장 제목으로 포함하는 <h1> 또는 <h2> 태그와 class="chapter" 속성을 가지고 있는 태그들입니다. 사용된 표현식은 요소들의 목록을 평가할 수 있어야 합니다. 장 탐지를 비활성화 하려면 표현식 "/"을 사용하세요. 이 특정 기능에 대한 더 자세한 도움말을 보려면 캘리버 사용자 설명서의 XPath 튜토리얼을 참조하세요.

--chapter-mark

탐지한 장을 표시할 방법을 지정합니다. "pagebreak" 값은 장 이전에 쪽 넘김을 삽입합니다. "rule" 값은 장 이전에 줄을 삽입합니다. "none" 값은 장 표시를 하지 않고, "both" 값은 쪽 넘김과 줄을 함께 사용합니다.

--disable-remove-fake-margins

어떤 문서들은 쪽 여백을 지정하기 위해 각 단락마다 매번 좌우 여백을 지정하는 경우가 있습니다. 캘리버는 이러한 여백을 탐지하여 제거하려고 시도합니다. 어떤 경우에는 제거되지 말아야할 여백이 제거되는 경우가 있습니다. 그런 경우에는 이 옵션을 비활성화 하세요.

--insert-metadata

책의 시작 부분에 메타데이터를 삽입합니다. 전자책 리더가 메타데이터를 직접 표시하거나 검색하는 기능을 지원하지 않을 때 유용합니다.

--page-breaks-before

XPath 표현식. 지정된 요소의 앞에 쪽 넘김이 삽입됩니다. 비활성화하려면 다음 표현식을 사용하세요: /

--prefer-metadata-cover

지정된 표지보다 우선적으로 원본 파일에서 감지된 표지를 사용합니다.

--remove-first-image

입력 전자책에서의 첫번째 이미지를 제거합니다. 입력 문서가 실제 표지가 아닌 표지 이미지를 가지고 있는 경우 유용합니다. 이런 경우, 이 옵션을 지정하지 않은 상태에서 캘리버에서 표지를 지정해놓으면 출력 문서는 두개의 표지 이미지를 가지게 되어버립니다.

--start-reading-at

문서에서의 읽기 시작 지점을 탐지하기 위한 XPath 표현식. 몇몇 프로그램(대표적으로 Kindle)은 책을 열 때 이 위치를 사용합니다. 이 특정 기능을 사용하는데 대한 더 많은 도움말은 캘리버 사용자 설명서의 XPath 튜토리얼을 참고하세요.

목차

목차의 자동 생성을 제어합니다. 기본적으로 원본 파일에 목차가 있는 경우, 자동으로 생성된 파일에 대한 환경설정에 사용됩니다.

--duplicate-links-in-toc

입력 문서의 링크에서 목차를 만들 때, 중복 항목을 허용합니다. 즉, 동일한 텍스트가 있는 항목이 다른 위치를 가리키는 경우 둘 이상의 항목을 허용합니다.

--level1-toc

목차의 1단계에 추가되어야할 모든 태그를 지정하는 XPath 표현식. 이 칸이 지정되어있다면, 다른 형태의 자동감지를 우선합니다. 예시를 보려면 캘리버 사용자 설명서 안의 XPath 튜토리얼을 참조하세요.

--level2-toc

목차의 2단계에 추가되어야할 모든 태그를 지정하는 XPath 표현식. 각 항목은 이전의 1단계 항목 아래에 추가됩니다. 예시를 보려면 캘리버 사용자 설명서 안의 XPath 튜토리얼을 참조하세요.

--level3-toc

목차의 3단계에 추가되어야할 모든 태그를 지정하는 XPath 표현식. 각 항목은 이전의 2단계 항목 아래에 추가됩니다. 예시를 보려면 캘리버 사용자 설명서 안의 XPath 튜토리얼을 참조하세요.

--max-toc-links

목차에 삽입할 링크의 최대 개수입니다. 비활성화하려면 0을 지정합니다. 기본값: 50. 챕터의 한계치보다 적게 발견할 경우만 링크들을 목차에 추가합니다.

--no-chapters-in-toc

자동 감지된 장(章)들을 목차에 추가하지 않습니다.

--toc-filter

지정한 정규 표현식과 일치하는 제목을 항목을 목차에서 삭제합니다. 일치하는 항목과 모든 자식(하위) 항목을 삭제합니다.

--toc-threshold

발견된 장(章)의 갯수가 이 숫자보다 적으면, 링크들을 목차에 추가합니다. 기본값: 6

--use-auto-toc

일반적으로 원본 파일이 이미 목차를 가지고 있다면, 자동으로 생성된 것보다 우선적으로 사용합니다. 이 옵션을 사용하면, 자동 생성된 항목이 항상 사용됩니다.

메타데이터

출력의 메타데이터를 지정하는 옵션입니다.

--author-sort

저자 순으로 정렬할 때 사용되어질 문자열입니다.

--authors

저자를 지정합니다. 다수의 저자들은 && 기호로 구분해야 합니다.

--book-producer

책 제작자를 지정합니다.

--comments

전자책의 설명을 지정합니다.

--cover

표지에 지정한 파일 또는 URL을 설정합니다

--isbn

책의 ISBN을 지정합니다.

--language

언어를 지정합니다.

--pubdate

출판 날짜를 설정합니다(시간대를 명시하지 않으면 로컬 시간대로 간주)

--publisher

책의 출판사를 지정합니다.

--rating

평점을 지정합니다. 1과 5 사이의 숫자여야 합니다.

--read-metadata-from-opf, --from-opf, -m

지정한 OPF 파일에서 메타데이터를 읽습니다. 이 파일에서 읽은 메타데이터가 원본 파일의 메타데이터를 덮어씁니다.

--series

이 책이 속하게 될 시리즈를 지정합니다.

--series-index

이 시리즈에서 책의 색인을 지정합니다.

--tags

책의 태그를 지정합니다. 쉼표(,)로 구분된 목록이어야 합니다.

--timestamp

책 타임스탬프 지정하기(더 이상 어디에도 사용되지 않음)

--title

제목을 지정합니다.

--title-sort

정렬을 위해 사용될 제목의 형태입니다.

디버그

변환 디버깅에 도움이 되는 옵션

--debug-pipeline, -d

변환 파이프라인의 각 단계 출력을 지정한 폴더에 저장합니다. 변환 과정의 어느 단계에서 버그가 발생하는지 확실하지 않을 때 유용합니다.

--verbose, -v

문서 안의 링크를 제거하지 않습니다. 일반 텍스트 출력에서는 링크가 항상 제거되므로, 위의 TXT 출력 서식 옵션이 none이 아닐 때만 유용합니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 명령행 인터페이스 »
- ebook-edit

ebook-edit

ebook-edit [opts] [path_to_ebook] [name_of_file_inside_book ...]

calibre 책 편집 도구를 실행합니다.
선택 사항으로 책 내부 파일 이름을 지정하면 해당 파일들을 자동으로 열어 편집할 수 있습니다.

:command:`ebook-edit`에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

[옵션]

--detach

제어 터미널이 있으면 분리합니다 (Linux 전용)

--help, -h

이 도움말 메시지를 표시한 후 종료

--select-text

편집을 위해 책을 열었을 때 선택할 텍스트

--version

프로그램의 버전을 표시한 후 종료합니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 명령행 인터페이스 »
- ebook-meta

ebook-meta

ebook-meta 전자책_파일 [옵션]

전자책 파일에서 메타데이터를 읽고 씁니다.

메타데이터 읽기를 지원하는 형식: azw, azw1, azw3, azw4, cb7, cbc, cbr, cbz, chm, docx, epub, fb2, fbz, html, htmlz, imp, kepub, lit, lrf, lrx, mobi, odt, oebzip, opf, pdb, pdf, pml, pmlz, pobi, prc, rar, rb, rtf, snb, tpz, txt, txtz, updb, zip

메타데이터 쓰기를 지원하는 형식: azw, azw1, azw3, azw4, docx, epub, fb2, fbz, htmlz, kepub, lrf, mobi, odt, pdb, pdf, prc, rtf, tpz, txtz

파일 형식마다 지원하는 메타데이터 종류가 다릅니다. 지원하지 않는 메타데이터를 어떤 파일 형식에 설정하려고 하면 그 메타데이터는 조용히 무시됩니다.

:command:`ebook-meta`에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

[옵션]

--author-sort

저자를 정렬할 때 사용될 문자열입니다. 이곳을 비워두고 저자 필드를 지정하면, 저자 필드에서 자동으로 생성될 것입니다.

--authors, -a

저자를 지정합니다. 저자가 여러명이면 & 문자로 구분해야 합니다. 저자의 이름의 순서는 성 이름이어야 합니다.

--book-producer, -k

책 제작자를 지정합니다.

--category

책 분류를 지정합니다.

--comments, -c

전자책의 설명을 지정합니다.

--cover

표지에 지정한 파일을 지정합니다.

--date, -d

출판일자를 지정합니다.

--disallow-rendered-cover

렌더링된 첫 페이지를 "기본 표지"로 사용하는 EPUB 같은 형식에서 그런 기본 표지를 허용하지 않음

--from-opf

지정한 OPF 파일에서 메타데이터를 읽어 전자책에 설정합니다. 명령줄에서 지정한 메타데이터가 OPF 파일에서 읽은 메타데이터를 덮어씁니다.

--get-cover

전자책에서 표지를 가져와서 지정한 파일로서 저장합니다.

--help, -h

이 도움말 메시지를 표시한 후 종료

--identifier

Set the identifiers for the book, can be specified multiple times. For example: --identifier uri:https://acme.com --identifier isbn:12345 To remove an identifier, specify no value, --identifier isbn: Note that for EPUB files, an identifier marked as the package identifier cannot be removed.

--index, -i

이 시리즈에서 책의 색인을 지정합니다.

--isbn

책의 ISBN을 지정합니다.

--language, -l

언어를 지정합니다.

--lrf-bookid

LRF 파일의 BookID를 지정

--publisher, -p

책의 출판사를 지정합니다.

--rating, -r

평점을 지정합니다. 1과 5 사이의 숫자여야 합니다.

--series, -s

이 책이 속하게 될 시리즈를 지정합니다.

--tags

책의 태그를 지정합니다. 쉼표(,)로 구분된 목록이어야 합니다.

--title, -t

제목을 지정합니다.

--title-sort

정렬을 위해 사용될 제목의 형태입니다. 이곳에 지정하지 않고 제목에 지정하면, 제목에서 자동으로 생성될 것입니다.

--to-opf

OPF 파일명을 지정합니다. 메타데이터는 OPF 파일에 기록될 것입니다.

--version

프로그램의 버전을 표시한 후 종료합니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 명령행 인터페이스 »
- ebook-polish

ebook-polish

ebook-polish [옵션] 입력_파일 [출력_파일]

책 손질하기는 사용자가 조심스럽게 만든 전자책에 완벽함을 더하기 위한 기능입니다.

책 손질하기 기능은 사용자의 전자책의 내부 코드에 대한 변경을 최소화하려 노력합니다.
변환 기능과 달리, CSS의 단순화, 파일명 변경, 글꼴 크기 변경, 여백 조정 등과 같은 작업을 하지 않습니다. 모든 동작은 원하는 효과를 위해 필요한 최소한의 작업만 수행합니다.

사용자는 전자책 생성 작업의 마지막 과정으로 이 도구를 사용하여야 합니다.

책 손질하기 기능은 AZW3 또는 EPUB 또는 KEPUB 형식의 파일에만 유효하다는 점을 유의하세요.

:command:`ebook-polish`에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

[옵션]

--add-soft-hyphens, -H

Add soft hyphens to all words in the book. This allows the book to be rendered better when the text is justified, in readers that do not support hyphenation.

--compress-images, -i

책 안에 포함된 이미지들을 이미지 품질에 영향이 가지 않도록 손실없이 압축하여 파일 크기를 줄입니다..

--cover, -c

유효하지 않은 이미지는 보통 손상되어 로드할 수 없는 이미지를 말합니다. 정상적인 이미지로 교체하거나 제거해야 합니다.

--download-external-resources, -d

Download external resources such as images, stylesheets, etc. that point to URLs instead of files in the book. All such resources will be downloaded and added to the book so that the book no longer references any external resources.

--embed-fonts, -e

문서에서 참조되고 아직 포함되지 않은 모든 글꼴을 포함합니다. 컴퓨터에서 글꼴을 검색하고, 글꼴이 발견되면 해당 글꼴이 문서에 포함됩니다. 이 문서에 사용된 글꼴을 포함할 수 있는 적절한 라이선스가 있는지 확인하십시오.

--help, -h

이 도움말 메시지를 표시한 후 종료

--jacket, -j

제목, 태그, 저자, 시리즈, 책 설명 등과 같은 모든 메타데이터를 담고 있는 "책 커버" 페이지를 책의 첫 부분에 삽입합니다. 기존의 책 커버 페이지가 있다면 대체됩니다.

--opf, -o

OPF 파일 경로입니다. 책의 메타데이터는 OPF 파일의 내용으로 업데이트됩니다.

--remove-jacket

기존에 삽입된 책 커버 페이지를 제거합니다

--remove-soft-hyphens

책의 모든 텍스트에서 소프트 하이픈을 제거합니다.

--remove-unused-css, -u

Remove all unused CSS rules from stylesheets and <style> tags. Some books created from production templates can have a large number of extra CSS rules that don't match any actual content. These extra rules can slow down readers that need to parse them all.

--smarten-punctuation, -p

일반 텍스트로 된 대시, 생략 문자, 따옴표, 다중 하이픈 등을 인쇄방법 상 올바른 형태로 변환합니다. 때때로 이 알고리즘이 부정확한 결과를 생성하기도 한다는 점을 유의하십시오. 특히 단축어의 첫부분에 작은따옴표가 쓰인 경우 그러합니다.

--subset-fonts, -f

글꼴 부분 설정은 책에 있는 해당 글꼴에서 사용된 문자만 포함하도록 포함된 글꼴을 줄이는 것을 의미합니다. 이렇게 하면 글꼴 파일의 크기가 크게 줄어듭니다(글꼴 파일 크기를 절반으로 줄이는 것이 일반적임). 예를 들어, 책이 머리말에 특정 글꼴을 사용하는 경우 하위 설정은 책의 실제 머리말에 있는 문자만 포함하도록 해당 글꼴을 줄입니다. 또는 책에 굵게 및 기울임꼴 버전의 글꼴이 포함되어 있지만 굵게 및 기울임꼴 텍스트가 상대적으로 드물거나 아예 없는 경우 굵게 및 기울임꼴 글꼴은 몇 글자로 줄어들거나 완전히 제거될 수 있습니다.. 하위설정 글꼴의 유일한 단점은, 나중에 책에 더 많은 텍스트를 추가하기로 결정한 경우 새로 추가 된 텍스트가 하위 집합 글꼴로 표시되지 않을 수 있다는 것입니다.

--upgrade-book, -U

가능하다면 책의 내부 구조를 업그레이드합니다. 예를 들어 EPUB 2 책을 EPUB 3 책으로 업그레이드합니다.

--verbose

디버깅에 유용한 더 자세한 출력을 생성합니다.

--version

프로그램의 버전을 표시한 후 종료합니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 명령행 인터페이스 »
- ebook-viewer

ebook-viewer

ebook-viewer [옵션] 파일

전자책을 봅니다.

:command:`ebook-viewer`에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

[옵션]

--continue

마지막으로 연 책 계속 읽기

--detach

제어 터미널이 있으면 분리합니다 (Linux 전용)

--force-reload

열려 있는 모든 책 강제로 다시 불러오기

--full-screen, --fullscreen, -f

지정하면 전자책 뷰어 창이 시작될 때 전체 화면으로 열리도록 시도합니다.

--help, -h

이 도움말 메시지를 표시한 후 종료

--new-instance

하나의 뷰어 창만 사용하도록 설정되어 있어도 새 뷰어 창 열기

--open-at

지정한 책을 열 위치입니다. 위치는 뷰어 제어의 이동->위치 동작을 사용해 얻을 수 있는 위치 또는 위치 정보입니다. 또는 toc:something 형식을 사용하면 "something" 문자열을 포함하는 첫 번째 목차 항목 위치에서 엽니다. toc-href:something 형식은 toc 노드의 href(내부 링크 대상)와 정확히 일치시킵니다. 부분 문자열 일치를 원하면 toc-href-contains:something 형식을 사용하십시오. ref:something 형식은 참조 모드의 참조를 사용합니다. search:something 형식은 책을 연 뒤 something을 검색합니다. regex:something 형식은 책을 연 뒤 정규식 something을 검색합니다.

--raise-window

지정하면 전자책 뷰어 창이 시작될 때 앞으로 나오도록 시도합니다.

--version

프로그램의 버전을 표시한 후 종료합니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 명령행 인터페이스 »
- fetch-ebook-metadata

fetch-ebook-metadata

fetch-ebook-metadata [options]

온라인 출처로부터 메타데이터를 가져옵니다. 반드시 적어도 제목, 저자 또는 ISBN 중 하나를 지정하여야 합니다.

:command:`fetch-ebook-metadata`에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

[옵션]

--allowed-plugin, -p

사용할 메타데이터 다운로드 플러그인 이름을 지정합니다. 기본적으로는 모든 메타데이터 플러그인이 사용됩니다. 여러 플러그인을 지정하려면 이 옵션을 여러 번 사용할 수 있습니다. 모든 플러그인 이름: Google, Google Images, Amazon.com, Edelweiss, Open Library

--authors, -a

책 저자

--cover, -c

파일명을 지정하세요. 표지는 그곳으로 저장됩니다. 이 옵션이 없는 경우 아무 표지도 다운로드되지 않습니다.

--help, -h

이 도움말 메시지를 표시한 후 종료

--identifier, -I

ASIN/Goodreads ID 등의 식별자입니다. 여러 식별자를 지정하려면 여러 번 지정할 수 있습니다. 예: --identifier asin:B0082BAJA0

--isbn, -i

책 ISBN

--opf, -o

메타데이터를 사람이 읽을 수 있는 텍스트 형태 대신 OPF 형식으로 출력합니다.

--timeout, -d

대기만료시간은 초단위입니다. 기본값은 30입니다

--title, -t

책 제목

--verbose, -v

로그를 콘솔로 출력 (stderr)

--version

프로그램의 버전을 표시한 후 종료합니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 명령행 인터페이스 »
- lrf2lrs

lrf2lrs

lrf2lrs book.lrf\nLRF 파일을 LRS(XML UTF-8 인코딩) 파일로 변환합니다

:command:`lrf2lrs`에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

[옵션]

--dont-output-resources

함께 포함된 이미지와 글꼴 파일을 디스크에 저장하지 않습니다.

--help, -h

이 도움말 메시지를 표시한 후 종료

--output, -o

LRS 파일 출력

--verbose

더 자세히 출력

--version

프로그램의 버전을 표시한 후 종료합니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 명령행 인터페이스 »
- lrfviewer

lrfviewer

lrfviewer [options] book.lrf

Read the LRF e-book book.lrf

:command:`lrfviewer`에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

[옵션]

--disable-hyphenation

하이픈 분리를 비활성화합니다. 렌더링 속도가 크게 빨라질 수 있습니다.

--help, -h

이 도움말 메시지를 표시한 후 종료

--profile

LRF 렌더러 성능 분석

--verbose

렌더링 과정에 대한 정보를 더 출력

--version

프로그램의 버전을 표시한 후 종료합니다.

--visual-debug

렌더링 엔진 디버깅용 시각 보조 기능 켜기

--white-background

문자 모드에서는 입력한 검색 텍스트를 지정한 필드에서 찾습니다. 찾은 모든 위치의 텍스트를 지정한 바꿀 텍스트로 치환합니다. 치환이 끝난 뒤에는 텍스트를 대문자, 소문자 또는 제목 형식으로 바꿀 수 있습니다. 대소문자 구분 상자를 선택하면 검색 텍스트가 정확히 일치해야 합니다. 선택하지 않으면 검색 텍스트는 대문자와 소문자를 모두 일치시킵니다

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 명령행 인터페이스 »
- lrs2lrf

lrs2lrf

lrs2lrf [options] file.lrs\nLRS 파일을 컴파일하여 LRF 파일로 만듭니다.

:command:`lrs2lrf`에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

[옵션]

--help, -h

이 도움말 메시지를 표시한 후 종료

--lrs

LRS를 LRS로 변환합니다. 디버깅에 유용합니다.

--output, -o

출력 파일의 경로

--verbose

자세한 처리

--version

프로그램의 버전을 표시한 후 종료합니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 명령행 인터페이스 »
- web2disk

web2disk

web2disk URL

여기서 URL의 예시는 다음과 같습니다: https://google.com

:command:`web2disk`에 공백이 포함된 인수를 전달할 때는 항상 인수를 따옴표로 묶으십시오. 예: “/some path/with spaces”

[옵션]

--base-dir, -d

URL을 저장할 기본 폴더입니다. 기본값은 .입니다

--delay

연속 가져오기 사이의 최소 간격(초)입니다. 기본값은 0초입니다

--dont-download-stylesheets

CSS 스타일시트를 다운로드하지 않음.

--encoding

다운로드하려고 하는 웹사이트에 대한 문자 인코딩입니다. 기본값은 추측하여 인코딩을 시도합니다.

--filter-regexp

이 정규식과 일치하는 링크는 모두 무시합니다. 이 옵션은 여러 번 지정할 수 있으며, 링크가 정규식 중 어느 하나와라도 일치하면 무시됩니다. 기본적으로는 무시되는 링크가 없습니다. filter 정규식과 match 정규식이 모두 지정되면 filter 정규식이 먼저 적용됩니다.

--help, -h

이 도움말 메시지를 표시한 후 종료

--match-regexp

이 정규식과 일치하는 링크만 따라갑니다. 이 옵션은 여러 번 지정할 수 있으며, 링크가 지정한 정규식 중 하나와라도 일치하면 따라갑니다. 기본적으로는 모든 링크를 따라갑니다.

--max-files, -n

다운로드할 파일의 최대 개수입니다. <a href> 태그에 있는 파일에만 적용합니다. 기본값은 9223372036854775807 입니다.

--max-recursions, -r

재귀할 최대 단계 수, 즉 따라갈 링크 깊이입니다. 기본값은 1입니다.

--timeout, -t

서버로부터의 응답을 기다릴 대기시간입니다(초 단위). 기본값: 10.0 초

--verbose

상세한 출력 정보를 표시합니다. 디버깅 시에 유용합니다.

--version

프로그램의 버전을 표시한 후 종료합니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 캘리버 개발 환경 설정

캘리버 개발 환경 설정

캘리버는 `GNU GPL v3 <https://www.gnu.org/licenses/gpl.html>`_에 따라 완전히 오픈 소스로 라이선스되어 있습니다. 이는 프로그램을 자유롭게 다운로드하고 원하는 대로 수정할 수 있다는 의미입니다. 이 섹션에서는 선택한 운영 체제에 캘리버 개발 환경을 설정하는 방법을 배웁니다. 캘리버는 주로 `Python <https://www.python.org>`_으로 작성되었으며, 속도와 시스템 인터페이스를 위해 일부 C/C++ 코드가 포함되어 있습니다. 캘리버에는 최소 Python 3.8이 필요합니다.

목차

- 설계 철학

- 코드 구성

- 코드 가져오기

- 변경 사항 제출하여 포함시키기

- Windows 개발 환경

- macOS 개발 환경

- Linux 개발 환경

- 동일한 컴퓨터에 별도의 “일반” 및 “개발” 캘리버 설치 갖기

- 디버깅 팁

- print 문 사용하기

- 대화형 Python 인터프리터 사용하기

- Python 디버거를 원격 디버거로 사용하기

- 즐겨 사용하는 Python IDE에서 디버거 사용하기

- 캘리버 Python 환경에서 임의 스크립트 실행하기

- 프로젝트에서 캘리버 사용하기

- 캘리버 바이너리 설치

- Linux에서 소스 설치

- 캘리버 다양한 부분의 API 문서

설계 철학

캘리버는 Unix 세계에 뿌리를 두고 있어 설계가 매우 모듈화되어 있습니다. 모듈들은 잘 정의된 인터페이스를 통해 서로 상호작용합니다. 이로 인해 캘리버에 새로운 기능을 추가하고 버그를 수정하기가 매우 쉬워 빠른 개발 속도를 유지할 수 있습니다. 이러한 뿌리 덕분에 캘리버는 모든 기능에 대한 포괄적인 명령줄 인터페이스를 갖추고 있으며, 이는 :doc:`generated/ko/cli-index`에 문서화되어 있습니다.

캘리버의 모듈식 설계는 ``플러그인``을 통해 표현됩니다. 캘리버 플러그인 작성에 대한 :ref:`튜토리얼 <customize>`이 있습니다. 예를 들어, 캘리버에 새로운 장치 지원을 추가하는 것은 일반적으로 장치 드라이버 플러그인 형태로 100줄 미만의 코드를 작성하는 것과 관련됩니다. `내장 드라이버 <https://github.com/kovidgoyal/calibre/tree/master/src/calibre/devices>`_를 둘러볼 수 있습니다. 마찬가지로, 새로운 변환 형식 지원을 추가하는 것은 입출력 형식 플러그인을 작성하는 것을 포함합니다. 모듈식 설계의 또 다른 예는 뉴스를 가져오기 위한 :ref:`레시피 시스템 <news>`입니다. 캘리버에 기능을 추가하기 위해 설계된 플러그인의 더 많은 예는 `플러그인 색인 <https://www.mobileread.com/forums/showthread.php?p=1362767#post1362767>`_을 참조하십시오.

코드 구성

모든 캘리버 Python 코드는 calibre 패키지에 있습니다. 이 패키지는 다음과 같은 주요 하위 패키지를 포함합니다

- devices - 모든 장치 드라이버. 일부 내장 드라이버를 살펴보면 작동 방식을 이해할 수 있습니다.

- 자세한 내용은: 장치 드라이버가 지원하는 인터페이스를 정의하는 ``devices.interface``와 USBMS 장치에 연결하는 일반 드라이버를 정의하는 ``devices.usbms``를 참조하십시오. 캘리버의 모든 USBMS 기반 드라이버는 이것을 상속합니다.

- e-books - 모든 전자책 변환/메타데이터 코드. 좋은 출발점은 ebook-convert 명령을 구동하는 모듈인 ``calibre.ebooks.conversion.cli``입니다. 변환 과정은 ``conversion.plumber``를 통해 제어됩니다. 형식 독립적 코드는 모두 ``ebooks.oeb``에 있고, 형식 종속적 코드는 ``ebooks.format_name``에 있습니다.

- 메타데이터 읽기, 쓰기, 다운로드는 모두 ``ebooks.metadata``에 있습니다

- 변환은 파이프라인에서 이루어지며, 파이프라인의 구조는 :ref:`conversion-introduction`을 참조하십시오. 파이프라인은 입력 플러그인, 다양한 변환, 출력 플러그인으로 구성됩니다. 파이프라인을 구성하고 구동하는 코드는 :file:`plumber.py`에 있습니다. 파이프라인은 압축 해제된 epub과 类似的 전자책 표현에서 작동하며, 매니페스트, 스파인, 목차, 가이드, html 콘텐츠 등이 포함됩니다. 이 표현을 관리하는 클래스는 ``ebooks.oeb.base``의 OEBBook입니다. 변환 중 책에 적용되는 다양한 변환은 :file:`oeb/transforms/.py`에 있습니다. 그리고 입력 및 출력 플러그인은 :file:`conversion/plugins/.py`에 있습니다.

- 전자책 편집은 다른 컨테이너 객체를 사용하여 이루어집니다. 이는 :ref:`polish_api`에 문서화되어 있습니다.

- db - 데이터베이스 백엔드. 캘리버 라이브러리에 대한 인터페이스는 :ref:`db_api`를 참조하십시오.

- 콘텐츠 서버: ``srv``는 캘리버 콘텐츠 서버입니다.

- gui2 - 그래픽 사용자 인터페이스. GUI 초기화는 ``gui2.main``과 ``gui2.ui``에서 이루어집니다. 전자책 뷰어는 ``gui2.viewer``에 있습니다. 전자책 편집기는 ``gui2.tweak_book``에 있습니다.

모든 다양한 캘리버 실행 파일의 진입점을 찾으려면 linux.py <https://github.com/kovidgoyal/calibre/blob/master/src/calibre/linux.py>`_에서 ``entry_points` 구조를 확인하십시오.

코드 이해에 도움이 필요하다면 `개발 포럼 <https://www.mobileread.com/forums/forumdisplay.php?f=240>`_에 게시하면 캘리버의 많은 개발자 중 한 명에게 도움을 받을 수 있을 것입니다.

코드 가져오기

캘리버 소스 코드를 두 가지 방법으로 얻을 수 있습니다: 버전 제어 시스템을 사용하거나 :website_base:`tarball <dist/src>`을 직접 다운로드하는 것입니다.

캘리버는 분산 버전 제어 시스템인 `Git <https://www.git-scm.com/>`_을 사용합니다. Git은 캘리버가 지원하는 모든 플랫폼에서 사용할 수 있습니다. Git을 설치한 후 다음 명령으로 캘리버 소스 코드를 얻을 수 있습니다:

git clone https://github.com/kovidgoyal/calibre.git

Windows에서는 전체 경로 이름이 필요하며, :file:`C:\Program Files\Git\git.exe`와 같은 형태가 됩니다.

캘리버는 매우 긴 소스 제어 기록을 가진 매우 큰 프로젝트이므로, 위 명령은 시간이 걸릴 수 있습니다 (인터넷 속도에 따라 10분에서 1시간).

코드를 더 빠르게 얻고 싶다면, 최신 릴리스의 소스 코드는 항상 :website_base:`아카이브 <dist/src>`로 사용할 수 있습니다.

브랜치를 최신 코드로 업데이트하려면 다음 명령을 사용하십시오:

git pull --no-edit

`GitHub <https://github.com/kovidgoyal/calibre>`_에서도 코드를 둘러볼 수 있습니다.

변경 사항 제출하여 포함시키기

작은 변경 사항만 만들 계획이라면, 변경 사항을 만들고 “병합 지시문”을 생성한 후 캘리버 `버그 트래커 <https://bugs.launchpad.net/calibre>`_의 티켓에 첨부할 수 있습니다. 이렇게 하려면 변경 사항을 만든 후 다음을 실행하십시오:

git commit -am "Comment describing your changes"
git format-patch origin/master --stdout > my-changes

이렇게 하면 현재 폴더에 my-changes 파일이 생성되며, 캘리버 `버그 트래커 <https://bugs.launchpad.net/calibre>`_의 티켓에 첨부하기만 하면 됩니다. 여기에는 수행한 모든 커밋이 포함됩니다. 일부 커밋만 보내려면 위의 ``origin/master``를 변경해야 합니다. 마지막 커밋만 보내려면 다음을 사용하십시오:

git format-patch HEAD~1 --stdout > my-changes

마지막 *n*개의 커밋을 보내려면 *1*을 *n*으로 바꾸십시오. 예를 들어, 마지막 3개 커밋의 경우:

git format-patch HEAD~3 --stdout > my-changes

``HEAD~n``을 사용할 때 병합을 포함하지 않도록 주의하십시오.

캘리버에 많은 개발을 할 계획이라면 GitHub [https://github.com] 계정을 만드는 것이 가장 좋은 방법입니다. 아래는 기본 캘리버 저장소에 포함되도록 풀 요청을 제출할 수 있는 방식으로 캘리버의 자체 포크를 설정하는 기본 안내입니다:

- 이 문서의 설명에 따라 컴퓨터에서 git 설정: Git 설정 [https://help.github.com/articles/set-up-git]

- 여기에 설명된 대로 GitHub 인증을 위한 ssh 키 설정: SSH 키 생성 [https://help.github.com/articles/generating-ssh-keys]

- https://github.com/kovidgoyal/calibre로 이동하여 포크 버튼을 클릭하십시오.

- 터미널에서 다음을 수행:

git clone git@github.com:<username>/calibre.git
git remote add upstream https://github.com/kovidgoyal/calibre.git

위의 <username>을 GitHub 사용자 이름으로 바꾸십시오. 이렇게 하면 포크가 로컬에 체크아웃됩니다.

- 원할 때마다 변경하고 커밋할 수 있습니다. 작업을 병합할 준비가 되면 다음을 수행합니다:

git push

그리고 ``https://github.com/<username>/calibre``로 이동하여 풀 요청 버튼을 클릭하면 병합 가능한 풀 요청이 생성됩니다.

- 다음을 수행하여 언제든지 기본 저장소의 코드로 로컬 복사본을 업데이트할 수 있습니다:

git pull upstream

캘리버 `개발 포럼 <https://www.mobileread.com/forums/forumdisplay.php?f=240>`_도 주시해야 합니다. 주요 변경을 하기 전에 포럼에서 논의하거나 Kovid에게 직접 연락해야 합니다 (그의 이메일 주소는 소스 코드 전체에 있습니다).

Windows 개발 환경

참고

위에서 설명한 대로 캘리버 소스 코드도 별도로 가져와야 합니다.

`Windows 설치 프로그램 <https://calibre-ebook.com/download_windows>`__을 사용하여 캘리버를 정상적으로 설치하십시오. 그런 다음 명령 프롬프트를 열고 이전에 체크아웃한 캘리버 코드 폴더로 이동하십시오. 예를 들어:

cd C:\Users\kovid\work\calibre

calibre는 src 및 resources 하위 폴더를 포함하는 폴더입니다.

다음 단계는 환경 변수 ``CALIBRE_DEVELOP_FROM``을 src 폴더의 절대 경로로 설정하는 것입니다. 따라서 위 예시를 따르면 ``C:Userskovidworkcalibresrc``가 됩니다. Windows에서 환경 변수를 설정하는 방법에 대한 `간단한 안내 <https://docs.python.org/using/windows.html#excursus-setting-environment-variables>`_가 있습니다.

환경 변수를 설정한 후 새 명령 프롬프트를 열고 다음 명령을 사용하여 올바르게 설정되었는지 확인하십시오:

echo %CALIBRE_DEVELOP_FROM%

이 환경 변수를 설정하면 캘리버가 이제 지정된 위치에서 모든 Python 코드를 불러옵니다.

이제 됐습니다! 이제 캘리버 코드 수정을 시작할 준비가 되었습니다. 예를 들어, 즐겨 사용하는 편집기에서 src\calibre\__init__.py 파일을 열고 다음 줄을 추가하십시오:

print("Hello, world!")

파일 상단 근처에. 이제 calibredb 명령을 실행하십시오. 출력의 첫 번째 줄이 ``Hello, world!``여야 합니다.

원한다면 무료 Microsoft Visual Studio 안에서도 캘리버 개발 환경을 설정할 수 있습니다. `여기 <https://www.mobileread.com/forums/showthread.php?t=251201>`_의 안내를 따르십시오.

macOS 개발 환경

참고

위에서 설명한 대로 캘리버 소스 코드도 별도로 가져와야 합니다.

제공된 `.dmg <https://calibre-ebook.com/download_osx>`__를 사용하여 캘리버를 정상적으로 설치하십시오. 그런 다음 터미널을 열고 이전에 체크아웃한 캘리버 코드 폴더로 이동하십시오. 예를 들어:

cd /Users/kovid/work/calibre

calibre는 src 및 resources 하위 폴더를 포함하는 폴더입니다. 캘리버 명령줄 도구는 캘리버 앱 번들 안에 있으며, :file:`/Applications/calibre.app/Contents/MacOS`에 있습니다. 명령줄 도구를 쉽게 실행하려면 이 폴더를 PATH 환경 변수에 추가해야 합니다.

다음 단계는 디버그 모드에서 캘리버를 실행할 때 환경 변수 ``CALIBRE_DEVELOP_FROM``을 src 폴더의 절대 경로로 설정하는 bash 스크립트를 만드는 것입니다.

일반 텍스트 파일 만들기:

#!/bin/sh
export CALIBRE_DEVELOP_FROM="/Users/kovid/work/calibre/src"
calibre-debug -g

이 파일을 :file:`/usr/local/bin/calibre-develop`로 저장한 다음 실행할 수 있도록 권한을 설정하십시오:

chmod +x /usr/local/bin/calibre-develop

이 작업을 완료한 후, 실행:

calibre-develop

캘리버가 시작될 때 터미널 창에 일부 진단 정보가 표시되어야 하며, GUI 창에서 버전 번호 뒤에 별표가 표시되어 소스에서 실행 중임을 나타내야 합니다.

Linux 개발 환경

참고

위에서 설명한 대로 캘리버 소스 코드도 별도로 가져와야 합니다.

캘리버는 주로 Linux에서 개발됩니다. 개발 환경을 설정하는 데 두 가지 선택지가 있습니다. 캘리버 바이너리를 정상적으로 설치하고 런타임 환경으로 사용하여 개발할 수 있습니다. 이 접근 방식은 Windows와 macOS에서 사용하는 것과 유사합니다. 또는 소스에서 캘리버를 설치할 수도 있습니다. 소스에서 개발 환경을 설정하는 방법은 소스 트리의 INSTALL 파일에 있습니다. 여기서는 권장 방법으로 바이너리를 런타임으로 사용하는 방법을 다루겠습니다.

`바이너리 설치 프로그램 <https://calibre-ebook.com/download_linux>`_을 사용하여 캘리버를 설치하십시오. 그런 다음 터미널을 열고 이전에 체크아웃한 캘리버 코드 폴더로 이동하십시오. 예를 들어:

cd /home/kovid/work/calibre

calibre는 src 및 resources 하위 폴더를 포함하는 폴더입니다.

다음 단계는 환경 변수 ``CALIBRE_DEVELOP_FROM``을 src 폴더의 절대 경로로 설정하는 것입니다. 따라서 위 예시를 따르면 ``/home/kovid/work/calibre/src``가 됩니다. 환경 변수를 설정하는 방법은 Linux 배포판과 사용 중인 셸에 따라 다릅니다.

참고

상류에서 제공하는 바이너리 설치 프로그램을 사용하는 것이 좋습니다. 배포판에서 제공하는 패키지를 사용하려는 경우, 대신 CALIBRE_PYTHON_PATH 및 CALIBRE_RESOURCES_PATH 변수를 사용하십시오. ``calibre-debug –paths``를 실행하여 이를 얻을 수 있습니다. 하지만 배포판 캘리버 패키지는 종종 완전히 깨져 있으며 완전히 지원되지 않는다는 점에 유의하십시오.

환경 변수를 설정한 후 새 터미널을 열고 다음 명령을 사용하여 올바르게 설정되었는지 확인하십시오:

echo $CALIBRE_DEVELOP_FROM

이 환경 변수를 설정하면 캘리버가 이제 지정된 위치에서 모든 Python 코드를 불러옵니다.

이제 됐습니다! 이제 캘리버 코드 수정을 시작할 준비가 되었습니다. 예를 들어, 즐겨 사용하는 편집기에서 src/calibre/__init__.py 파일을 열고 다음 줄을 추가하십시오:

print("Hello, world!")

파일 상단 근처에. 이제 calibredb 명령을 실행하십시오. 출력의 첫 번째 줄이 ``Hello, world!``여야 합니다.

동일한 컴퓨터에 별도의 “일반” 및 “개발” 캘리버 설치 갖기

캘리버 소스 트리는 매우 안정적이며 거의 깨지지 않지만, 별도의 테스트 라이브러리에서 소스로 실행하고 일상 라이브러리에서는 릴리스된 캘리버 버전을 실행해야 한다고 느끼면, .bat 파일이나 셸 스크립트를 사용하여 캘리버를 시작하면 쉽게 이를 달성할 수 있습니다. 아래 예시는 .bat 파일을 사용하여 Windows에서 이 작업을 수행하는 방법을 보여줍니다 (다른 플랫폼에 대한 안내는 동일하며, .bat 파일 대신 셸 스크립트를 사용하십시오)

일상 라이브러리로 캘리버 릴리스 버전을 시작하려면:

calibre-normal.bat:

calibre.exe "--with-library=C:\path\to\everyday\library folder"

calibre-dev.bat:

set CALIBRE_DEVELOP_FROM=C:\path\to\calibre\checkout\src
calibre.exe "--with-library=C:\path\to\test\library folder"

디버깅 팁

Python은 훌륭한 내성(introspection) 기능을 가진 동적 타입 언어입니다. Kovid는 디버거를 한 번도 사용하지 않고 캘리버 핵심 코드를 작성했습니다. 캘리버 코드를 디버깅하는 여러 전략이 있습니다:

print 문 사용하기

이것은 Kovid가 가장 좋아하는 디버깅 방법입니다. 관심 지점에 print 문을 삽입하고 터미널에서 프로그램을 실행하기만 하면 됩니다. 예를 들어, 터미널에서 GUI를 시작할 수 있습니다:

calibre-debug -g

마찬가지로, 전자책 뷰어를 시작할 수 있습니다:

calibre-debug -w /path/to/file/to/be/viewed

전자책 편집기는 다음과 같이 시작할 수 있습니다:

calibre-debug --edit-book /path/to/be/edited

대화형 Python 인터프리터 사용하기

다음 두 줄의 코드를 삽입하여 해당 지점에서 대화형 Python 세션을 시작할 수 있습니다:

from calibre import ipython
ipython(locals())

명령줄에서 실행하면 로컬로 정의된 변수(로컬 범위의 변수)에 접근할 수 있는 대화형 Python 인터프리터가 시작됩니다. 대화형 프롬프트는 객체 속성에 대한 Tab 완성 기능도 갖추고 있으며, dir(), type(), repr() 등과 같은 다양한 Python 내성 기능을 사용할 수 있습니다.

Python 디버거를 원격 디버거로 사용하기

내장 Python 디버거(pdb)를 명령줄에서 원격 디버거로 사용할 수 있습니다. 먼저 관심 있는 캘리버 코드 지점에서 원격 디버거를 시작하십시오. 예를 들어:

from calibre.rpdb import set_trace
set_trace()

그런 다음 캘리버를 정상적으로 실행하거나 이전 섹션에서 설명한 calibre-debug 명령 중 하나를 사용하여 실행하십시오. 코드의 위 지점에 도달하면 캘리버가 중단되어 디버거가 연결되기를 기다립니다.

이제 터미널이나 명령 프롬프트를 열고 다음 명령을 사용하여 디버깅 세션을 시작하십시오:

calibre-debug -c "from calibre.rpdb import cli; cli()"

`Python 표준 라이브러리 문서의 pdb 모듈 <https://docs.python.org/library/pdb.html#debugger-commands>`_에서 Python 디버거 사용 방법에 대해 읽을 수 있습니다.

참고

기본적으로 원격 디버거는 포트 4444에 연결을 시도합니다. 위의 set_trace()와 cli() 함수 모두에 port 매개변수를 전달하여 이를 변경할 수 있습니다: set_trace(port=1234) 및 cli(port=1234).

참고

Python 디버거는 여러 스레드를 처리할 수 없으므로, 각 스레드마다 한 번씩 set_trace를 호출해야 하며, 매번 다른 포트 번호를 사용해야 합니다.

즐겨 사용하는 Python IDE에서 디버거 사용하기

즐겨 사용하는 Python IDE에서 내장 디버거를 사용할 수 있습니다 (원격 디버깅을 지원하는 경우). 첫 번째 단계는 캘리버 src 체크아웃을 IDE의 ``PYTHONPATH``에 추가하는 것입니다. 즉, 위에서 ``CALIBRE_DEVELOP_FROM``으로 설정한 폴더가 IDE의 ``PYTHONPATH``에도 있어야 합니다.

그런 다음 IDE의 원격 디버거 모듈을 캘리버 소스 코드 체크아웃의 src 하위 폴더에 넣으십시오. 관심 지점에서 (예: 기본 함수에서) 캘리버에 원격 디버거를 시작하는 데 필요한 코드를 추가하십시오. 그런 다음 캘리버를 정상적으로 실행하십시오. 이제 IDE가 캘리버 내부에서 실행 중인 원격 디버거에 연결할 수 있어야 합니다.

캘리버 Python 환경에서 임의 스크립트 실행하기
calibre-debug 명령은 캘리버 모듈에 접근할 수 있는 자체 코드를 실행하기 위한 몇 가지 편리한 스위치를 제공합니다:

calibre-debug -c "some Python code"

명령줄에서 약간의 코드 조각을 테스트하는 데 유용합니다. Python 인터프리터에 대한 -c 스위치와 동일한 방식으로 작동합니다:

calibre-debug myscript.py

자신의 Python 스크립트를 실행하는 데 사용할 수 있습니다. 캘리버 환경이 완전히 초기화되어 스크립트의 모든 캘리버 코드를 사용할 수 있다는 점을 제외하고는 Python 인터프리터에 스크립트를 전달하는 것과 같은 방식으로 작동합니다. 스크립트와 함께 명령줄 인수를 사용하려면 다음 형식을 사용합니다:

calibre-debug myscript.py -- --option1 arg1

``–``는 이후의 모든 인수를 스크립트에 전달합니다.

프로젝트에서 캘리버 사용하기

Python 프로젝트에서 직접 캘리버 함수/코드를 사용할 수 있습니다. 두 가지 방법이 있습니다:

캘리버 바이너리 설치

캘리버의 바이너리 설치가 있는 경우, 다음과 같이 캘리버와 함께 번들로 제공되는 Python 인터프리터를 사용할 수 있습니다:

calibre-debug /path/to/your/python/script.py -- arguments to your script

Linux에서 소스 설치

위의 기술을 사용하는 것 외에도, Linux에서 소스 설치를 수행하면 다음과 같이 직접 캘리버를 가져올 수도 있습니다:

import init_calibre
import calibre

print(calibre.__version__)

다른 캘리버 모듈/패키지보다 먼저 init_calibre 모듈을 가져오는 것이 필수적입니다. 이 모듈이 인터프리터를 캘리버 코드를 실행하도록 설정하기 때문입니다.

캘리버 다양한 부분의 API 문서

- 레시피 API 문서
- 플러그인용 API 문서
- 데이터베이스 인터페이스에 대한 API 문서
- 전자책 편집 도구용 API 문서

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 캘리버 개발 환경 설정 »
- 데이터베이스 인터페이스에 대한 API 문서

데이터베이스 인터페이스에 대한 API 문서

이 API는 스레드 안전합니다(다중 리더, 단일 라이터 잠금 방식을 사용합니다). 이 API에 다음과 같이 접근할 수 있습니다:

from calibre.library import db
db = db('Path to calibre library folder').new_api

메인 calibre GUI의 일부인 calibre 플러그인에 있다면, 대신 다음과 같이 접근합니다:

db = self.gui.current_db.new_api

class calibre.db.cache.Cache(backend, library_database_instance=None)[소스]

calibre 라이브러리의 metadata.db 파일에 대한 인메모리 캐시입니다. 이 클래스는 데이터베이스에 접근하기 위한 스레드 안전 API 역할도 합니다. 인메모리 캐시는 최대 성능을 위해 정규형으로 유지됩니다.

SQLITE는 metadata.db를 안정적으로 읽고 쓰기 위한 수단으로만 사용됩니다. 모든 테이블 읽기/정렬/검색/캐싱 로직은 재구현되었습니다. 이는 최대 성능과 유연성을 위해 필요했습니다.

class EventType(*values)

book_created = 4

데이터베이스에 새 책 레코드가 생성될 때, 책 ID를 유일한 인자로 전달합니다

book_edited = 8

책 형식이 편집될 때, 인자: (book_id, fmt)

books_removed = 5

책이 데이터베이스에서 제거될 때, 책 ID 목록을 유일한 인자로 전달합니다

format_added = 2

책에 형식이 추가될 때, 인자: (book_id, format)

formats_removed = 3

책에서 형식이 제거될 때, 인자: (책 ID에서 해당 책에서 제거된 형식 집합으로의 매핑)

indexing_progress_changed = 9

인덱싱 진행률이 변경될 때

items_removed = 7

태그나 저자 같은 항목이 일부 책에서 제거될 때. 인자: (필드 이름, 영향을 받은 책 ID, 제거된 항목의 ID)

items_renamed = 6

태그나 저자 같은 항목이 일부 또는 모든 책에서 이름이 변경될 때. 인자: (필드 이름, 영향을 받은 책 ID, 이전 항목 ID에서 새 항목 ID로의 매핑)

links_changed = 11

항목과 연결된 링크가 변경될 때, 인자: (필드 이름, item_ids)

metadata_changed = 1

일부 책의 일부 메타데이터가 변경될 때, 인자: (변경된 필드 이름, 영향을 받은 책 ID 집합)

notes_changed = 10

항목과 연결된 노트가 변경될 때, 인자: (필드 이름, item_ids)

add_books(books, add_duplicates=True, apply_import_tags=True, preserve_uuid=False, run_hooks=True, dbapi=None)[소스]

지정된 책을 라이브러리에 추가합니다. books는 2-튜플의 반복 가능 객체여야 하며, 각 2-튜플은 (mi, format_map) 형식입니다. 여기서 mi는 Metadata 객체이고 format_map은 {fmt: path_or_stream} 형식의 사전입니다. 예: {'EPUB': '/path/to/file.epub'}.

두 목록의 쌍을 반환합니다: ids, duplicates. ``ids``는 데이터베이스에 새로 생성된 모든 책의 책 ID를 포함합니다. ``duplicates``는 :meth:`has_book`이 사용하는 간단한 중복 감지 휴리스틱에 따라 데이터베이스에 이미 존재하는 모든 책의 :code:`(mi, format_map)`을 포함합니다.

add_custom_book_data(name, val_map, delete_first=False)[소스]

name에 대한 데이터를 추가합니다. 여기서 val_map은 book_id에서 값으로의 매핑입니다. delete_first가 True이면 name에 대해 이전에 저장된 모든 데이터가 제거됩니다.

add_extra_files(book_id, map_of_relpath_to_stream_or_path, replace=True, auto_rename=False)[소스]

추가 데이터 파일을 추가합니다

add_format(book_id, fmt, stream_or_path, replace=True, run_hooks=True, dbapi=None)[소스]

지정된 책에 형식을 추가합니다. 형식이 성공적으로 추가되면 True를 반환합니다.

매개변수:

- replace – True이면 기존 형식을 교체하고, 그렇지 않으면 형식이 이미 존재하는 경우 False를 반환합니다.

- run_hooks – True이면 파일 형식 플러그인이 형식 추가 전후에 실행됩니다.

- dbapi – 내부 전용입니다.

add_listener(event_callback_function, check_already_added=False)[소스]

이 데이터베이스에서 특정 작업이 수행된 후 호출될 콜백 함수를 등록합니다. 함수는 세 개의 인자를 받아야 합니다: (EventType, library_id, event_type_specific_data)

add_notes_resource(path_or_stream_or_data, name: str, mtime: float = None) → int[소스]

지정된 리소스를 추가하여 노트에서 참조할 수 있도록 하고 해당 콘텐츠 해시를 반환합니다

all_annotation_styles()[소스]

모든 내장 주석 스타일의 튜플을 반환합니다.

all_annotation_types()[소스]

데이터베이스의 모든 주석 유형의 튜플을 반환합니다.

all_annotation_users()[소스]

주석이 있는 모든 (user_type, 사용자 이름)의 튜플을 반환합니다.

all_annotations(restrict_to_user=None, limit=None, annotation_type=None, annotation_style=None, ignore_removed=False, restrict_to_book_ids=None)[소스]

지정된 조건과 일치하는 모든 주석의 튜플을 반환합니다. `ignore_removed`는 제거된(삭제된) 주석도 반환할지 여부를 제어합니다. 제거된 주석은 주석 병합에 사용되는 골격일 뿐입니다.

all_annotations_for_book(book_id)[소스]

지정된 book_id에 대한 모든 주석을 format, user_type, user, annotation 키를 가진 사전으로 포함하는 튜플을 반환합니다. 여기서 annotation은 주석 데이터입니다.

all_book_ids(type=<class 'frozenset'>)[소스]

모든 알려진 책 ID의 고정 집합입니다.

all_field_for(field, book_ids, default_value=None)[소스]

field_for와 같지만, 여러 책에 대해 동시에 작동합니다

all_field_ids(name)[소스]

필드 ``name``의 모든 값에 대한 ID의 고정 집합입니다.

all_field_names(field)[소스]

모든 필드 이름의 고정 집합입니다(다대일 및 다대다 필드에만 사용해야 합니다)

annotation_count_for_book(book_id)[소스]

데이터베이스에서 사용 가능한 지정된 책의 주석 수를 반환합니다.

annotations_map_for_book(book_id, fmt, user_type='local', user='viewer')[소스]

지정된 book_id, format, user 및 user_type에 대한 주석 유형 -> 주석 데이터의 매핑을 반환합니다.

author_data(author_ids=None) → dict[int, dict[str, str]][소스]

저자 데이터를 키가 있는 사전으로 반환: 이름, 정렬, 링크

지정된 ID를 가진 저자를 찾을 수 없으면 빈 사전이 반환됩니다. author_ids가 None이면 모든 저자의 데이터가 반환됩니다.

author_sort_from_authors(authors, key_func=<function make_change_case_func.<locals>.change_case>)[소스]

저자 목록이 주어지면, 계산된 문자열보다 저자와 연결된 저자 정렬을 우선하여 저자들의 author_sort 문자열을 반환합니다.

author_sorts(author_ids=None) → dict[int, str][소스]

지정된 저자들의 저자 정렬을 반환합니다.

지정된 ID를 가진 저자를 찾을 수 없으면 빈 사전이 반환됩니다. author_ids가 None이면 모든 저자의 데이터가 반환됩니다.

books_for_field(name, item_id)[소스]

항목이 필드 ``name``에 속하는 경우, ``item_id``로 식별된 항목과 연결된 모든 책을 반환합니다.

반환값은 책 ID의 집합이며, 항목이나 필드가 존재하지 않으면 빈 집합입니다.

books_in_virtual_library(vl, search_restriction=None, virtual_fields=None)[소스]

지정된 가상 라이브러리에 있는 책의 집합을 반환합니다

compress_covers(book_ids, jpeg_quality=100, progress_callback=None)[소스]

지정된 책의 표지 이미지를 압축합니다. 압축 품질이 100이면 무손실 압축을 수행하고, 그 외의 경우 손실 압축을 수행합니다.

진행률 콜백은 처리된 각 책에 대해 book_id와 이전 및 새 크기와 함께 호출됩니다. 오류가 발생하면 새 크기는 오류 세부 정보가 포함된 문자열이 됩니다.

copy_cover_to(book_id, dest, use_hardlink=False, report_file_size=None)[소스]

표지를 파일과 유사한 객체 ``dest``에 복사합니다. 표지가 존재하지 않거나 dest가 현재 표지와 같은 파일이면 False를 반환합니다. dest는 경로일 수도 있으며, 이 경우 경로가 현재 경로와 다를 때(대소문자 구분 고려)만 표지가 해당 경로에 복사됩니다.

copy_format_to(book_id, fmt, dest, use_hardlink=False, report_file_size=None)[소스]

형식 ``fmt``를 파일과 유사한 객체 ``dest``에 복사합니다. 지정된 형식이 존재하지 않으면 NoSuchFormat 오류를 발생시킵니다. dest는 (파일에 대한) 경로일 수도 있으며, 이 경우 경로가 현재 경로와 다를 때(대소문자 구분 고려)만 형식이 해당 경로에 복사됩니다.

cover(book_id, as_file=False, as_image=False, as_path=False, as_pixmap=False)[소스]

표지 이미지 또는 None을 반환합니다. 기본적으로 표지를 바이트 문자열로 반환합니다.

경고: as_path를 사용하면 표지를 임시 파일에 복사하고 해당 임시 파일의 경로를 반환합니다. 사용이 끝나면 임시 파일을 삭제해야 합니다.

매개변수:

- as_file – True이면 이미지를 열린 파일 객체(SpooledTemporaryFile)로 반환합니다

- as_image – True이면 이미지를 QImage 객체로 반환합니다

- as_pixmap – True이면 이미지를 QPixmap 객체로 반환합니다

- as_path – True이면 이미지를 임시 파일을 가리키는 경로로 반환합니다

data_for_find_identical_books()[소스]

데이터베이스에 접근하지 않는 워커 프로세스에서 :meth:`find_identical_books`를 구현하는 데 사용할 수 있는 데이터를 반환합니다. 구현은 db.utils를 참조하십시오.

data_for_has_book()[소스]

:meth:`has_book`에 사용하기에 적합한 데이터를 반환합니다. 이는 데이터베이스에 접근하지 않는 워커 프로세스에서 :meth:`has_book`의 구현에 사용할 수 있습니다.

delete_annotations(annot_ids)[소스]

지정된 ID의 주석을 삭제합니다.

delete_custom_book_data(name, book_ids=())[소스]

name에 대한 데이터를 삭제합니다. 기본적으로 모든 데이터를 삭제하며, 일부 book_id에 대한 데이터만 삭제하려면 book_id 목록을 전달하십시오.

delete_trash_entry(book_id, category)[소스]

휴지통에서 항목을 삭제합니다. 여기서 category는 책의 경우 ‘b’, 형식의 경우 ‘f’입니다.

embed_metadata(book_ids, only_fmts=None, report_error=None, report_progress=None)[소스]

지정된 book_ids의 모든 형식에 있는 메타데이터를 데이터베이스의 현재 메타데이터로 업데이트합니다.

expire_old_trash()[소스]

너무 오래된 휴지통 항목을 만료시킵니다

export_note(field, item_id) → str[소스]

노트를 data: URL로 임베드된 이미지가 포함된 단일 HTML 문서로 내보냅니다

fast_field_for(field_obj, book_id, default_value=None)[소스]

field_for와 같지만, 필드 객체를 얻기 위한 추가 조회를 피합니다

field_for(name, book_id, default_value=None)[소스]

``book_id``로 식별된 책의 필드 ``name``의 값을 반환합니다. 해당 책이 존재하지 않거나 필드 ``name``에 대해 정의된 값이 없거나 해당 필드가 존재하지 않으면 ``default_value``가 반환됩니다.

``default_value``는 title, title_sort, authors, author_sort, series_index에는 사용되지 않습니다. 이 필드들은 항상 데이터베이스에 값이 있기 때문입니다. ``default_value``는 모든 사용자 정의 열에 사용됩니다.

is_multiple 필드의 반환값은 값이 발견되지 않아도 항상 튜플입니다(즉, default_value는 무시됩니다). 예외적으로 identifiers의 반환값은 항상 사전입니다. 반환된 튜플은 항상 링크 순서, 즉 생성된 순서로 반환됩니다.

field_ids_for(name, book_id)[소스]

``book_id``로 식별된 책에서 필드 ``name``이 가진 값에 대한 ID를 튜플로 반환합니다. 값이 없거나 해당 책이 없거나 해당 필드가 없으면 빈 튜플이 반환됩니다.

field_supports_notes(field=None) → bool[소스]

지정된 필드가 노트를 지원하면 True를 반환합니다. 필드가 None이면 노트를 지원하는 모든 필드의 frozenset을 반환합니다.

find_identical_books(mi, search_restriction='', book_ids=None)[소스]

mi의 저자를 포함하고 같은 제목을 가진 책을 찾습니다(제목은 퍼지 매칭됩니다). :meth:`data_for_find_identical_books`도 참조하십시오.

format(book_id, fmt, as_file=False, as_path=False, preserve_filename=False)[소스]

전자책 형식을 바이트 문자열로 반환하거나, 형식이 존재하지 않거나 전자책 파일에 대한 쓰기 권한이 없으면 `None`을 반환합니다.

매개변수:

- as_file – True이면 전자책 형식이 파일 객체로 반환됩니다. 파일 객체는 SpooledTemporaryFile이므로, 형식을 다른 파일에 복사하려는 경우 성능을 위해 :meth:`copy_format_to`를 대신 사용하십시오.

- as_path – 형식 파일을 임시 파일에 복사하고 해당 임시 파일의 경로를 반환합니다

- preserve_filename – True이고 경로를 반환하는 경우 파일 이름은 라이브러리에서 사용되는 것과 동일합니다. 이를 사용하면 반복 호출 시 동일한 임시 파일이 반환됩니다(매번 다시 생성됨)

format_abspath(book_id, fmt)[소스]

format 형식의 전자책 파일에 대한 절대 경로를 반환합니다. 이는 이 API의 스레드 안전성을 깨뜨리므로 거의 사용해서는 안 됩니다. 대신 :meth:`copy_format_to`를 사용하십시오.

현재 calibredb list, 뷰어, 책 편집, 원본 형식과의 compare_format, 다른 프로그램으로 열기, 대량 메타데이터 편집 및 카탈로그(get_data_as_dict() 경유)에서만 사용됩니다.

뷰어, 다른 프로그램으로 열기, 책 편집을 제외하고는, 이 호출의 결과로 파일 쓰기 I/O를 수행하는 것은 없는 것으로 보입니다.

format_hash(book_id, fmt)[소스]

지정된 책의 지정된 형식에 대한 해시를 반환합니다. 해시 종류는 백엔드에 따라 다르지만 일반적으로 SHA-256입니다.

format_metadata(book_id, fmt, allow_cache=True, update_db=False)[소스]

지정된 책의 지정된 형식에 대한 경로, 크기, mtime을 반환합니다. 직접 접근하면 이 API의 스레드 안전 보장이 깨지므로, 반드시 필요한 경우가 아니면 path를 사용해서는 안 됩니다. 대신 copy_format_to() 메서드를 사용하십시오.

매개변수:

- allow_cache – ``True``이면 캐시된 값이 사용되고, 그렇지 않으면 느린 파일 시스템 접근이 수행됩니다. 이 API 외부에서 파일 시스템에 접근이 수행된 경우 캐시 값이 오래되었을 수 있습니다.

- update_db – ``True``이면 이 책에 대한 데이터베이스의 max_size 필드가 업데이트됩니다.

formats(book_id, verify_formats=True)[소스]

지정된 책의 모든 형식의 튜플을 반환합니다. verify_formats가 True이면 파일이 디스크에 존재하는지 확인합니다.

get_all_items_that_have_notes(field_name=None) → set[int] | dict[str, set[int]][소스]

지정된 필드에서 노트가 있는 모든 항목의 item_ids를 반환하거나, field_name이 None이면 모든 필드에 대해 반환합니다

get_all_link_maps_for_book(book_id)[소스]

book_id로 식별된 책이 참조하는 모든 필드의 모든 링크를 반환합니다. book_id가 존재하지 않으면 메서드가 {}를 반환합니다.

예시: 저자 A에 링크 X, 저자 B에 링크 Y, 태그 S에 링크 F, 태그 T에 링크 G가 있다고 가정합니다. 책 1에 저자 A와 태그 T가 있으면 이 메서드는 {‘authors’:{‘A’:’X’}, ‘tags’:{‘T’, ‘G’}}를 반환합니다. 책 2의 저자가 A도 B도 아니고 태그가 없으면 이 메서드는 {}를 반환합니다.

매개변수:

book_id – 해당 책 ID입니다.

반환:

해당 책에 대해 field_value가 비어 있지 않은 링크 값을 가진 모든 필드에 대한 {field: {field_value, link_value}, …}

get_book_path(book_id, sep='/', unsafe=False)[소스]

주어진 ID에 대한 상대적 책 경로를 반환합니다. 디렉터리 구분자를 선택할 수 있으므로 이것을 권장하며, 기본값은 OS의 것을 사용합니다. unsafe가 True이면 book_id가 라이브러리에 없는 경우 None을 반환할 수 있습니다.

get_categories(sort='name', book_ids=None, already_fixed=None, first_letter_sort=False, uncollapsed_categories=None)[소스]

태그 브라우저를 구현하는 데 내부적으로 사용됩니다

get_custom_book_data(name, book_ids=(), default=None)[소스]

name에 대한 데이터를 가져옵니다. 기본적으로 모든 book_id의 데이터를 반환하며, 일부 데이터만 원하면 book_id 목록을 전달하십시오. book_id에서 값으로의 매핑을 반환합니다. 특정 값을 디코딩할 수 없으면 해당 값에 기본값을 사용합니다.

get_id_map(field)[소스]

지정된 필드에 대한 ID 번호에서 값으로의 매핑을 반환합니다. 필드는 다대일 또는 다대다 필드여야 하며, 그렇지 않으면 ValueError가 발생합니다.

get_ids_for_custom_book_data(name)[소스]

name에 데이터가 있는 book_id의 집합을 반환합니다.

get_item_id(field, item_name, case_sensitive=False)[소스]

item_name에 대한 항목 ID를 반환하거나 찾지 못하면 None을 반환합니다. 여러 이름에 대해 조회를 수행하는 경우 이 함수는 매우 느리므로 get_item_ids()나 get_item_name_map()을 사용하십시오. 마찬가지로 대소문자 구분 조회가 대소문자 무시 조회보다 빠릅니다.

get_item_ids(field, item_names, case_sensitive=False)[소스]

item_name을 항목 ID에 매핑하는 사전을 반환하거나 None을 반환합니다

get_item_name(field, item_id)[소스]

지정된 필드에서 item_id로 지정된 항목의 항목 이름을 반환합니다. :meth:`get_id_map`도 참조하십시오.

get_item_name_map(field, normalize_func=None)[소스]

항목 값에서 ID로의 매핑을 반환합니다

get_link_map(for_field)[소스]

제공된 필드에 대한 링크의 사전을 반환합니다.

매개변수:

for_field – 링크 맵이 필요한 필드의 조회 이름

반환:

비어 있지 않은 링크에 대한 {field_value:link_value, …}

get_metadata(book_id, get_cover=False, get_user_categories=True, cover_as_data=False)[소스]

book_id로 식별된 책의 메타데이터를 calibre.ebooks.metadata.book.base.Metadata 객체로 반환합니다. 형식 목록은 검증되지 않음에 유의하십시오. get_cover가 True이면 표지가 반환되며, mi.cover로 임시 파일 경로가 반환되거나 cover_as_data가 True이면 mi.cover_data로 반환됩니다.

get_next_series_num_for(series, field='series', current_indices=False)[소스]

다음 시리즈 번호 생성을 제어하는 다양한 환경 설정을 고려하여 지정된 시리즈의 다음 시리즈 인덱스를 반환합니다.

매개변수:

- field – 시리즈와 유사한 필드(기본값은 내장 시리즈 열)

- current_indices – True이면 대신 book_id에서 현재 series_index 값으로의 매핑을 반환합니다.

get_notes_resource(resource_hash) → dict | None[소스]

리소스 데이터와 이름이 포함된 사전을 반환하거나, 지정된 해시의 리소스를 찾을 수 없으면 None을 반환합니다

get_pages(book_id: int) → Pages | None[소스]

지정된 책의 페이지 수 정보를 반환합니다

get_proxy_metadata(book_id)[소스]

:meth:`get_metadata`와 비슷하지만, 필요할 때만 데이터베이스에서 값을 읽는 ProxyMetadata 객체를 반환합니다. 반환된 메타데이터 객체에서 소수의 필드만 접근해야 할 때 get_metadata보다 훨씬 빠릅니다.

get_usage_count_by_id(field)[소스]

다대일 또는 다대다 필드여야 하는 지정된 필드의 모든 값에 대한 ID에서 사용 횟수로의 매핑을 반환합니다.

has_book(mi)[소스]

데이터베이스에 전달된 Metadata 객체와 같은 제목의 항목이 포함되어 있으면 True를 반환합니다. 비교는 대소문자를 구분하지 않습니다. :meth:`data_for_has_book`도 참조하십시오.

has_format(book_id, fmt)[소스]

형식이 디스크에 존재하면 True를 반환합니다

has_id(book_id)[소스]

지정된 book_id가 데이터베이스에 존재하면 True를 반환합니다

import_note(field, item_id, path_to_html_file, path_is_data=False)[소스]

이전에 내보낸 노트나 임의의 HTML 파일을 지정된 항목의 노트로 가져옵니다

init()[소스]

백엔드의 데이터로 이 캐시를 초기화합니다.

items_with_notes_in_book(book_id: int) → dict[str, dict[int, str]][소스]

지정된 책의 각 필드에 대해 연결된 노트가 있는 항목의 필드에서 항목으로의 사전을 반환합니다

link_for(field, item_id)[소스]

지정된 항목의 링크가 있으면 반환하고, 찾지 못하면 None을 반환합니다

list_extra_files(book_id, use_cache=False, pattern='') → tuple[ExtraFile, ...][소스]

책 디렉터리의 추가 파일에 대한 정보를 가져옵니다.

매개변수:

- book_id – 책에 대한 데이터베이스 책 ID

- pattern – 검색할 파일 이름의 패턴입니다. 빈 패턴은 모든 추가 파일과 일치합니다. 패턴은 구분자로 /를 사용해야 합니다. DATA_FILE_PATTERN 상수를 사용하여 데이터 디렉터리 내의 파일과 일치시킬 수 있습니다.

반환:

지정된 패턴과 일치하는 모든 추가 파일의 튜플입니다. 튜플의 각 요소는 ExtraFile(relpath, file_path, stat_result)입니다. 여기서 relpath는 /를 구분자로 사용하여 책 디렉터리에 대한 파일의 상대 경로입니다. stat_result는 파일에 대해 os.stat()를 호출한 결과입니다.

mark_for_pages_recount(book_id: int = 0) → None[소스]

모든 책을 페이지 재계산 대상으로 표시합니다

merge_annotations_for_book(book_id, fmt, annots_list, user_type='local', user='viewer')[소스]

지정된 주석을 book_id, fm, user_type, user에 대한 기존 주석에 병합합니다.

merge_extra_files(dest_id, src_ids, replace=False)[소스]

src_ids의 추가 파일을 dest_id에 병합합니다. 충돌하는 파일은 자동 이름이 변경되며, replace=True이면 교체됩니다.

move_book_from_trash(book_id)[소스]

휴지통 디렉터리에서 책을 복원합니다

move_format_from_trash(book_id, fmt)[소스]

휴지통 디렉터리에서 형식을 복원합니다

multisort(fields, ids_to_sort=None, virtual_fields=None)[소스]

정렬된 책 ID의 목록을 반환합니다. ids_to_sort가 None이면 모든 책 ID가 반환됩니다.

fields는 (field_name, ascending=True 또는 False) 형식의 2-튜플 목록이어야 합니다. 가장 중요한 필드가 첫 번째 2-튜플입니다.

notes_data_for(field, item_id) → str[소스]

모든 노트 데이터를 사전으로 반환하거나 노트가 존재하지 않으면 None을 반환합니다

notes_for(field, item_id) → str[소스]

노트 문서를 반환하거나 찾지 못하면 빈 문자열을 반환합니다

notes_resources_used_by(field, item_id)[소스]

지정된 항목의 노트가 사용하는 모든 리소스의 리소스 해시 집합을 반환합니다

pages_needs_scan(books: Iterable[int] = ()) → set[int][소스]

페이지 수 업데이트를 위해 스캔이 필요한 것으로 표시된 책의 하위 집합을 반환합니다(또는 비어 있으면 모든 책)

pref(name, default=None, namespace=None, get_default_from_defaults=False)[소스]

지정된 환경 설정의 값을 반환하거나, 환경 설정이 설정되지 않은 경우 ``default``로 지정된 값을 반환합니다.

queue_pages_scan(book_id: int = 0, force: bool = False, by_user: bool = True) → None[소스]

스캔이 필요한 모든 책의 페이지 수를 업데이트하는 스캔을 시작합니다. book_id가 지정되면 해당 책만 스캔되며 항상 스캔됩니다. `force`가 True이면 기존 페이지 값(있는 경우)이 무시되어 기존 값이 최신 상태여도 책이 강제로 다시 스캔됩니다.

read_backup(book_id)[소스]

책의 OPF 메타데이터 백업을 바이트 문자열로 반환하거나 해당 백업이 존재하지 않으면 None을 반환합니다.

remove_books(book_ids, permanent=False)[소스]

book_ids로 지정된 책을 데이터베이스에서 제거하고 해당 형식 파일을 삭제합니다. ``permanent``가 False이면 형식 파일이 라이브러리별 휴지통 디렉터리에 배치됩니다.

remove_extra_files(book_id: int, relpaths: Iterable[str], permanent=False) → dict[str, Exception | None][소스]

지정된 추가 파일을 휴지통으로 보내거나 영구적으로 삭제합니다.

remove_formats(formats_map, db_only=False)[소스]

지정된 형식을 지정된 책에서 제거합니다.

매개변수:

- formats_map – book_id에서 책에서 제거할 형식 목록으로의 매핑입니다.

- db_only – True이면 데이터베이스에서 형식에 대한 기록만 제거하고, 실제 형식 파일을 파일 시스템에서 삭제하지 않습니다.

반환:

책 ID에서 해당 책의 파일 시스템에서 실제로 삭제된 형식의 집합으로의 매핑

remove_items(field, item_ids, restrict_to_book_ids=None)[소스]

지정된 필드에서 지정된 ID를 가진 모든 항목을 삭제합니다. 영향을 받은 책 ID의 집합을 반환합니다. ``restrict_to_book_ids``는 선택적 책 ID 집합입니다. 지정된 경우 해당 책에서만 항목이 제거됩니다.

rename_extra_files(book_id, map_of_relpath_to_new_relpath, replace=False)[소스]

추가 데이터 파일의 이름을 변경합니다

rename_items(field, item_id_to_new_name_map, change_index=True, restrict_to_book_ids=None)[소스]

태그나 시리즈 같은 다대일 또는 다대다 필드의 항목 이름을 변경합니다.

매개변수:

- change_index – 시리즈와 같은 필드에서 이름을 변경할 때 series_index 값도 함께 변경합니다.

- restrict_to_book_ids – 이름 변경이 수행될 책 ID의 선택적 집합이며, 기본값은 모든 책입니다.

restore_book(book_id, mi, last_modified, path, formats, annotations=())[소스]

파일 시스템에 이미 존재하는 책에 대한 데이터베이스의 책 항목을 복원합니다

restore_original_format(book_id, original_fmt)[소스]

이전에 저장된 ORIGINAL_FORMAT에서 지정된 형식을 복원합니다(있는 경우). 성공 시 True를 반환합니다. 성공적인 복원 후 ORIGINAL_FORMAT이 삭제됩니다.

property safe_read_lock

안전한 읽기 잠금은 스레드에 이미 쓰기 잠금이 있으면 아무것도 하지 않고, 그렇지 않으면 읽기 잠금을 획득하는 잠금입니다. 이는 복합 열 존재 시 검색 캐시를 업데이트할 때 발생할 수 있는 DowngradeLockErrors를 방지하기 위해 필요합니다. 검색 캐시를 업데이트하면 배타적 잠금을 보유하지만, 복합 열을 검색하면 공유 잠금을 얻으려는 ProxyMetadata를 통해 필드 값을 읽어야 합니다. 이를 트리거하는 다른 시나리오도 있을 수 있습니다.

이 속성은每次 접근 시 새로운 잠금 객체를 반환합니다. 이 잠금 객체는 (성능을 위해) 재귀적이지 않으며, ``with cache.safe_read_lock:``와 같은 with 문에서만 사용해야 합니다. 그렇지 않으면 문제가 발생합니다.

save_original_format(book_id, fmt)[소스]

지정된 형식의 복사본을 ORIGINAL_FORMAT으로 저장하고, 기존 ORIGINAL_FORMAT이 있으면 덮어씁니다.

search(query, restriction='', virtual_fields=None, book_ids=None, allow_templates=True)[소스]

지정된 쿼리로 데이터베이스를 검색하여 일치하는 책 ID의 집합을 반환합니다.

매개변수:

- restriction – 지정된 쿼리에 AND로 연결되는 제한 조건입니다. 제한 조건은 캐시되므로, a AND b 검색이 제한 조건 b로 a를 검색하는 것보다 느립니다.

- virtual_fields – 내부적으로 사용됩니다(on_device 같은 가상 필드를 검색하기 위해).

- book_ids – None이 아닌 경우, 모든 책을 검색하는 대신 검색할 책 ID의 집합입니다.

search_annotations(fts_engine_query, use_stemming=True, highlight_start=None, highlight_end=None, snippet_size=None, annotation_type=None, restrict_to_book_ids=None, restrict_to_user=None, ignore_removed=False, annotation_style=None)[소스]

지정된 전체 텍스트 쿼리와 일치하는 주석의 튜플을 반환합니다.

search_notes(fts_engine_query='', use_stemming=True, highlight_start=None, highlight_end=None, snippet_size=None, restrict_to_fields=(), return_text=True, result_type=<class 'tuple'>, process_each_result=None, limit=None)[소스]

FTS 색인을 사용하여 노트의 텍스트를 검색합니다. 쿼리가 비어 있으면 모든 노트를 반환합니다.

set_annotations_for_book(book_id, fmt, annots_list, user_type='local', user='viewer')[소스]

지정된 book_id, fmt, user_type, user에 대한 모든 주석을 설정합니다.

set_conversion_options(options, fmt='PIPE')[소스]

options는 {book_id:conversion_options} 형식의 매핑이어야 합니다

set_cover(book_id_data_map)[소스]

이 책의 표지를 설정합니다. 데이터는 QImage, QPixmap, 파일 객체 또는 바이트 문자열일 수 있습니다. None일 수도 있으며, 이 경우 기존 표지가 제거됩니다.

set_field(name, book_id_to_val_map, allow_case_change=True, do_path_update=True)[소스]

``name``으로 지정된 필드의 값을 설정합니다. 변경으로 영향을 받은 모든 책 ID의 집합을 반환합니다.

매개변수:

- book_id_to_val_map – 적용되어야 할 값으로의 book_ids의 매핑입니다.

- allow_case_change – True이면 다대일 또는 다대다 필드의 대소문자가 변경됩니다. 예를 들어, 책에 태그 ``tag1``이 있고 다른 책의 태그를 ``Tag1``으로 설정하면, allow_case_change가 True이면 두 책 모두 태그 ``Tag1``을 가지게 되고, 그렇지 않으면 두 책 모두 태그 ``tag1``을 가지게 됩니다.

- do_path_update – 내부적으로 사용되며, 변경해서는 안 됩니다.

set_link_map(field, value_to_link_map, only_set_if_no_existing_link=False)[소스]

필드의 항목 값에 대한 링크를 설정합니다. 참고: 이 메서드는 value_to_link_map에 없는 값은 변경하지 않습니다

매개변수:

- field – 조회 이름

- value_to_link_map – dict(field_value:link, …). 이것들은 필드 ID가 아닌 값임에 유의하십시오.

반환:

링크 설정으로 변경된 책

set_metadata(book_id, mi, ignore_errors=False, force_changes=False, set_title=True, set_authors=True, allow_case_change=False)[소스]

Metadata 객체 `mi`에서 책 `id`의 메타데이터를 설정합니다
force_changes=True로 설정하면 mi에 빈 값이 있어도 set_metadata가 필드를 강제로 업데이트합니다. 이 경우 ‘None’과 ‘empty’가 구분됩니다. mi.XXX가 None이면 XXX는 교체되지 않고, 그렇지 않으면 교체됩니다. 태그, 식별자, 표지 속성은 특수한 경우입니다. 태그와 식별자는 None으로 설정할 수 없으므로 force_changes가 true이면 항상 교체됩니다. mi에 책이 가지기를 원하는 값이 포함되어 있어야 합니다. 표지는 새 표지가 제공되면 항상 변경되지만 삭제되지는 않습니다. 또한 force_changes는 제목이나 저자 설정에는 영향을 미치지 않습니다.

set_notes_for(field, item_id, doc: str, searchable_text: str = '', resource_hashes=(), remove_unused_resources=False) → int[소스]

노트 문서를 설정합니다. 검색 가능한 텍스트가 문서와 다르면 searchable_text로 지정하십시오. 문서가 리소스를 참조하면 해당 해시가 resource_hashes에 있어야 합니다. remove_unused_resources를 True로 설정하면 사용하지 않는 리소스를 정리할 수 있습니다. 노트를 업데이트하면 해당 노트에 관련된 리소스가 자동으로 정리됨에 유의하십시오.

set_pages(book_id: int, pages: int = 0, algorithm: int = 0, format: str = '', format_size: int = 0) → None[소스]

지정된 책의 페이지 수 정보를 설정합니다

set_pref(name, val, namespace=None)[소스]

지정된 환경 설정을 지정된 값으로 설정합니다. :meth:`pref`도 참조하십시오.

split_if_is_multiple_composite(f, val)[소스]

f가 복합 열 조회 키이고 열이 is_multiple이면 v를 고유한 비어 있지 않은 값으로 분할합니다. 비교는 대소문자를 구분합니다. 순서는 유지되지 않습니다. 프록시 메타데이터 필드 getter(예: 태그)와의 호환성을 위해 list()를 반환합니다.

tags_older_than(tag, delta=None, must_have_tag=None, must_have_authors=None)[소스]

지정된 시간보다 오래된 태그 ``tag``를 가진 모든 책의 ID를 반환합니다. tag 비교는 대소문자를 구분하지 않습니다.

매개변수:

- delta – timedelta 객체 또는 None입니다. None이면 태그를 가진 모든 ID가 반환됩니다.

- must_have_tag – None이 아닌 경우 일치 목록이 해당 태그를 가진 책으로 제한됩니다

- must_have_authors – 저자 목록입니다. None이 아닌 경우 일치 목록이 해당 저자를 가진 책으로 제한됩니다(대소문자 무시).

unretire_note_for(field, item_id) → int[소스]

지정된 항목에 대해 이전에 은퇴된 노트를 은퇴 해제합니다. 항목이 데이터베이스에서 제거될 때 노트가 은퇴됩니다

update_annotations(annot_id_map)[소스]

주석을 업데이트합니다.

user_categories_for_books(book_ids, proxy_metadata_map=None)[소스]

지정된 책의 사용자 카테고리를 반환합니다. proxy_metadata_map은 선택적이며, 해당 책의 ProxyMetadata 객체가 이미 존재하는 맥락에서 성능 향상에 유용합니다. book_ids에서 해당 ProxyMetadata 객체로의 매핑이어야 합니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 캘리버 개발 환경 설정 »
- 전자책 편집 도구용 API 문서

전자책 편집 도구용 API 문서

전자책 편집 도구는 책을 HTML + 리소스 파일 모음으로 나타내는 calibre.ebooks.oeb.polish.container.Container 객체와, 그 컨테이너에서 여러 작업을 수행하는 데 사용할 수 있는 다양한 도구로 이루어져 있습니다. 모든 도구는 여러 calibre.ebooks.oeb.polish.* 모듈에 있는 모듈 수준 함수 형태로 제공됩니다.

다음과 같이 경로에 있는 책의 컨테이너 객체를 얻을 수 있습니다:

from calibre.ebooks.oeb.polish.container import get_container
container = get_container('Path to book file', tweak_mode=True)

전자책 편집기용 플러그인을 작성하는 경우, 현재 편집 중인 책의 컨테이너는 다음과 같이 얻습니다:

from calibre.gui2.tweak_book import current_container
container = current_container()
if container is None:
report_error # No book has been opened yet

Container 객체
class calibre.ebooks.oeb.polish.container.Container(rootpath=None, opfpath=None, log=<calibre.utils.logging.Log object>, clone_data=None)[소스]

컨테이너는 파일이 가득한 폴더와 OPF 파일로 구성된 열린 전자책을 나타냅니다. 여기에는 두 가지 중요한 개념이 있습니다:

- 루트 폴더. 전자책의 기준 위치입니다. 전자책의 모든 파일은 이 폴더 또는 그 하위 폴더 안에 있습니다.

- 이름(Name): 루트 폴더를 기준으로 한 책 파일의 경로입니다. 항상 POSIX 구분자를 사용하며 따옴표로 이스케이프되지 않습니다. 책 안의 파일을 식별하는 정규 식별자로 생각하면 됩니다. 컨테이너 객체의 대부분 메서드는 이름을 사용합니다. 이름은 항상 NFC 유니코드 정규화 형식입니다.

- 복제(Clone): 컨테이너 객체는 디스크 상에서 효율적인 복제를 지원하며, 전자책 편집기의 체크포인트 구현에 사용됩니다. 이를 제대로 동작하게 하려면 파일 시스템의 파일에 직접 접근하면 안 됩니다. 대신 raw_data() 또는 :meth:`open`을 사용해 책 안의 구성 파일을 읽고 쓰십시오.

href와 이름 사이를 변환할 때는 이 클래스에서 제공하는 메서드를 사용하십시오. 이 메서드들은 모든 href가 따옴표로 이스케이프된 것으로 가정합니다.

abspath_to_name(fullpath, root=None)[소스]

:attr:`root`를 기준으로 한 정규 이름으로 절대 경로를 변환합니다.

매개변수:

root – 기준 폴더입니다. 기본값으로 이 컨테이너 객체의 루트가 사용됩니다.

add_file(name, data=b'', media_type=None, spine_index=None, modify_name_if_needed=False, process_manifest_item=None, suggested_id='')[소스]

이 컨테이너에 파일을 추가합니다. 파일에 대한 항목이 OPF manifest와 spine에 자동으로 생성됩니다(파일이 텍스트 문서인 경우).

add_name_to_manifest(name, process_manifest_item=None, suggested_id='')[소스]

지정한 이름의 파일에 대해 manifest 항목을 추가합니다. manifest id를 반환합니다.

add_properties(name, *properties)[소스]

이름으로 식별된 manifest 항목에 지정한 속성을 추가합니다.

apply_unique_properties(name, *properties)[소스]

지정한 속성이 이름으로 식별된 manifest 항목에만 설정되도록 합니다. 이름에 None을 전달하면 모든 항목에서 해당 속성을 제거합니다.

book_type = 'oeb'

책의 형식입니다(EPUB 파일은 epub, AZW3 파일은 azw3).

commit(outpath=None, keep_parsed=False)[소스]

변경된 모든 파싱 객체를 파일 시스템에 기록하고, outpath에 전자책 파일을 출력합니다.

매개변수:

- output – 저장된 전자책 파일을 기록할 경로입니다. None이면 원본 책 파일의 경로를 사용합니다.

- keep_parsed – True이면 커밋된 항목의 파싱된 표현이 캐시에 유지됩니다.

commit_item(name, keep_parsed=False)[소스]

파싱된 객체를 디스크에 커밋합니다(직렬화되어 실제 파일에 기록됩니다). ``keep_parsed``가 True이면 파싱된 표현이 캐시에 유지됩니다. 참고: parsed()

dirty(name)[소스]

name에 해당하는 파싱 객체를 dirty 상태로 표시합니다. 참고: parsed().

exists(name)[소스]

정규 이름에 해당하는 파일/폴더가 존재할 때만 True를 반환합니다. 이 함수는 기반 OS 파일 시스템의 한계, 특히 대소문자 구분 여부의 영향을 받습니다. 따라서 대소문자를 구분하지 않는 파일 시스템에서는 name의 대소문자가 실제 파일과 달라도 True를 반환할 수 있습니다. 참고: has_name()

filesize(name)[소스]

지정한 정규 이름이 나타내는 파일의 바이트 단위 크기를 반환합니다. dirty 상태의 파싱 객체도 자동으로 처리합니다. 참고: parsed()

generate_item(name, id_prefix=None, media_type=None, unique_href=True)[소스]

지정한 이름으로부터 href를 파생하여 manifest에 항목을 추가합니다. href와 id의 고유성이 자동으로 보장됩니다. 생성된 항목을 반환합니다.

get_file_path_for_processing(name, allow_modification=True)[소스]

open()과 비슷하지만, 열린 파일 객체 대신 파일 경로를 반환합니다.

property guide_type_map

guide 유형에서 정규 이름으로의 매핑

has_name(name)[소스]

지정한 것과 동일한 정규 이름을 가진 파일이 존재할 때만 True를 반환합니다. :meth:`exists`와 달리 이 메서드는 항상 대소문자를 구분합니다.

href_to_name(href, base=None)[소스]

href(기준은 base)를 이름으로 변환합니다. base는 이름이어야 하며, None이면 self.root를 사용합니다.

insert_into_xml(parent, item, index=None)[소스]

항목을 parent에 삽입합니다(index가 None이면 끝에 추가). 들여쓰기는 자동으로 맞춥니다. 자기 닫힘 항목에서만 동작합니다.

is_dir = False

이 컨테이너가 압축 해제된 책(디렉터리)을 나타내는 경우

iterlinks(name, get_line_numbers=True)[소스]

name 안의 모든 링크를 반복합니다. get_line_numbers가 True이면 (link, line_number, offset) 형태의 결과를 반환합니다. 여기서 line_number는 링크가 나타나는 줄 번호이고, offset은 해당 줄 시작부터의 문자 수입니다. offset이 0이 아니면 실제로는 여러 줄에 걸칠 수도 있습니다.

make_name_unique(name)[소스]

`name`이 이 책에 이미 존재하지 않도록 합니다. 이미 존재하면 충돌하지 않는 수정된 이름을 반환합니다.

manifest_has_name(name)[소스]

manifest에 name에 해당하는 항목이 있으면 True를 반환합니다.

property manifest_id_map

manifest id에서 정규 이름으로의 매핑

manifest_items_of_type(predicate)[소스]

media-type이 predicate와 일치하는 모든 manifest 항목의 이름입니다. `predicate`는 집합, 리스트, 문자열 또는 단일 인수를 받는 함수가 될 수 있으며, 함수인 경우 media-type을 인수로 호출됩니다.

manifest_items_with_property(property_name)[소스]

지정한 속성을 가진 모든 manifest 항목

property manifest_type_map

manifest media-type에서 해당 media-type의 정규 이름 목록으로의 매핑

property mi

이 책의 메타데이터를 담은 Metadata 객체입니다. 이 객체는 이 속성을 요청할 때마다 즉시 생성되므로 꼭 필요할 때만 사용하십시오.

name_to_abspath(name)[소스]

정규 이름을 OS 종속 절대 경로로 변환합니다.

name_to_href(name, base=None)[소스]

이름을 base 기준 href로 변환합니다. base는 이름이어야 하며, None이면 self.root가 기준으로 사용됩니다.

property names_that_must_not_be_changed

절대 이름을 바꿔서는 안 되는 이름 집합입니다. 전자책 파일 형식에 따라 달라집니다.

property names_that_must_not_be_removed

컨테이너에서 절대 삭제하면 안 되는 이름 집합입니다. 전자책 파일 형식에 따라 달라집니다.

property names_that_need_not_be_manifested

manifest에서 누락되어도 허용되는 이름 집합입니다. 전자책 파일 형식에 따라 달라집니다.

open(name, mode='rb')[소스]

name이 가리키는 파일을 직접 읽기/쓰기로 엽니다. 파일이 dirty 상태이면 커밋되고 파스 캐시에서 제거된다는 점에 유의하십시오. 다시 파싱된 버전에 접근하기 전에 반드시 이 파일 처리를 끝내야 합니다. 그렇지 않으면 문제가 생길 수 있습니다.

property opf

파싱된 OPF 파일

opf_get_or_create(name)[소스]

지정한 이름의 첫 번째 XML 요소를 반환하거나, 없으면 opf:package 요소 아래에 생성한 뒤 반환하는 편의 메서드입니다.

property opf_version

OPF의 <package> 요소에 설정된 버전

property opf_version_parsed

OPF의 <package> 요소에 설정된 버전을 정수 튜플로 나타낸 값

opf_xpath(expr)[소스]

OPF 파일에서 XPath 식을 평가하는 편의 메서드입니다. opf:`와 `dc: 네임스페이스 접두사가 미리 정의되어 있습니다.

parsed(name)[소스]

name으로 지정한 파일의 파싱된 표현을 반환합니다. HTML/XML 파일은 lxml 트리를, CSS 파일은 css_parser 스타일시트를 반환합니다. 성능을 위해 파싱 객체는 캐시된다는 점에 유의하십시오. 파싱 객체를 변경한 경우 컨테이너가 캐시를 갱신할 수 있도록 반드시 dirty`를 호출해야 합니다. 참고: :meth:`replace()

raw_data(name, decode=True, normalize_to_nfc=True)[소스]

지정한 이름의 파일에 해당하는 원시 데이터를 반환합니다.

매개변수:

- decode – True이고 파일의 MIME 유형이 텍스트 기반이면, 원시 바이트 대신 디코딩된 유니코드 객체를 반환합니다.

- normalize_to_nfc – True이면 반환된 유니코드 객체가 EPUB 및 AZW3 파일 형식에서 요구되는 NFC 정규화 형식으로 정규화됩니다.

relpath(path, base=None)[소스]

절대 경로(os 구분자 사용)를 base 기준 상대 경로로 변환합니다(base 기본값은 self.root). 이 상대 경로는 *이름*이 아닙니다. 이름으로 변환하려면 :meth:`abspath_to_name`을 사용하십시오.

remove_from_spine(spine_items, remove_if_no_longer_in_spine=True)[소스]

지정한 항목(정규 이름 기준)을 spine에서 제거합니다. ``remove_if_no_longer_in_spine``가 True이면 항목을 spine에서만 빼는 것이 아니라 책에서도 삭제합니다.

remove_from_xml(item)[소스]

parent에서 항목을 제거하고 들여쓰기를 맞춥니다(자기 닫힘 항목에서만 동작).

remove_item(name, remove_from_guide=True)[소스]

이름으로 식별된 항목을 이 컨테이너에서 제거합니다. 이 작업은 OPF manifest, guide, spine에 있는 모든 참조와 내부 캐시의 항목도 함께 제거합니다.

rename(current_name, new_name)[소스]

파일 이름을 current_name에서 new_name으로 바꿉니다. 파일이 들어 있는 폴더가 바뀌면 파일 내부의 모든 링크 기준을 자동으로 다시 맞춥니다. 단, 이 파일을 참조할 수 있는 다른 파일 안의 링크는 갱신되지 않습니다. 성능 때문이며, 이런 갱신은 한 번에 일괄 처리해야 합니다.

replace(name, obj)[소스]

name에 해당하는 파싱 객체를 obj로 교체합니다. obj는 동일한 종류의 객체여야 합니다. 즉, HTML/XML이면 lxml 트리, CSS 파일이면 css_parser 스타일시트여야 합니다.

replace_links(name, replace_func)[소스]

replace_func를 사용해 name 안의 모든 링크를 교체합니다. replace_func는 URL을 받아 치환된 URL을 반환하는 호출 가능 객체여야 합니다. 또한 실제 치환이 일어나면 True로 설정되는 ‘replaced’ 속성을 가져야 합니다. 이런 호출 객체는 LinkReplacer`와 :class:`LinkRebaser 클래스를 사용하면 편리하게 만들 수 있습니다.

serialize_item(name)[소스]

파싱된 객체(정규 이름으로 식별됨)를 바이트 문자열로 변환합니다. 참고: parsed()

set_spine(spine_items)[소스]

spine을 spine_items로 설정합니다. spine_items는 (name, linear) 형태의 반복 가능 객체여야 합니다. 이름 중 하나라도 manifest에 없으면 오류가 발생합니다.

property spine_items

책의 spine에 있는 각 항목의 경로를 반환하는 반복자입니다. 참고: spine_iter, spine_items

property spine_iter

책의 spine에 있는 각 항목에 대해 item, name, is_linear를 반환하는 반복자입니다. item은 lxml 요소, name은 정규 파일 이름이며, 항목이 linear이면 is_linear는 True입니다. 참고: spine_names, spine_items

property spine_names

책의 spine에 있는 각 항목에 대해 name과 is_linear를 반환하는 반복자입니다. 참고: spine_iter, spine_items

컨테이너 안의 구성 파일 관리

calibre.ebooks.oeb.polish.replace.replace_links(container, link_map, frag_map=<function <lambda>>, replace_in_opf=False)[소스]

컨테이너 안의 파일을 가리키는 링크를 교체합니다. 컨테이너의 모든 파일을 순회하면서 지정한 링크를 변경합니다.

매개변수:

- link_map – 이전 정규 이름에서 새 정규 이름으로의 매핑입니다. 예: {'images/old.png': 'images/new.png'}

- frag_map – (name, anchor) 두 인수를 받아 새 anchor를 반환하는 호출 가능 객체입니다. HTML 파일의 anchor를 바꿔야 할 때 유용합니다. 기본값은 아무 작업도 하지 않습니다.

- replace_in_opf – False이면 OPF 파일의 링크는 교체하지 않습니다.

calibre.ebooks.oeb.polish.replace.rename_files(container, file_map)[소스]

컨테이너 안의 파일 이름을 바꾸고, 해당 파일을 가리키는 모든 링크를 자동으로 갱신합니다.

매개변수:

file_map – 이전 정규 이름에서 새 정규 이름으로의 매핑입니다. 예: {'text/chapter1.html': 'chapter1.html'}

calibre.ebooks.oeb.polish.replace.get_recommended_folders(container, names)[소스]

지정한 파일 이름에 대해 권장되는 폴더를 반환합니다. 권장값은 컨테이너 안에서 같은 유형의 파일이 가장 많이 위치한 폴더를 기준으로 합니다. 특정 유형의 파일이 하나도 없으면 OPF 파일이 들어 있는 폴더를 권장 폴더로 간주합니다.

예쁘게 정리하기 및 파싱 오류 자동 수정

calibre.ebooks.oeb.polish.pretty.fix_html(container, raw)[소스]

raw 문자열로 표현된 HTML의 파싱 오류를 수정합니다. 수정은 HTML5 파싱 알고리즘을 사용해 수행됩니다.

calibre.ebooks.oeb.polish.pretty.fix_all_html(container)[소스]

컨테이너 안의 모든 HTML 파일의 파싱 오류를 수정합니다. 수정은 HTML5 파싱 알고리즘을 사용해 수행됩니다.

calibre.ebooks.oeb.polish.pretty.pretty_html(container, name, raw)[소스]

raw 문자열로 표현된 HTML을 보기 좋게 정리합니다.

calibre.ebooks.oeb.polish.pretty.pretty_css(container, name, raw)[소스]

raw 문자열로 표현된 CSS를 보기 좋게 정리합니다.

calibre.ebooks.oeb.polish.pretty.pretty_xml(container, name, raw)[소스]

raw 문자열로 표현된 XML을 보기 좋게 정리합니다. ``name``이 OPF 이름이면 OPF 전용 추가 정리도 수행됩니다.

calibre.ebooks.oeb.polish.pretty.pretty_all(container)[소스]

컨테이너 안의 모든 HTML/CSS/XML 파일을 보기 좋게 정리합니다.

책 재킷 관리

calibre.ebooks.oeb.polish.jacket.remove_jacket(container)[소스]

기존 재킷이 있으면 제거합니다. 기존 재킷이 없으면 False를 반환합니다.

calibre.ebooks.oeb.polish.jacket.add_or_replace_jacket(container)[소스]

책의 메타데이터로 새 재킷을 만들거나 기존 재킷을 교체합니다. 기존 재킷이 교체된 경우 True를 반환합니다.

파일 분할 및 병합

calibre.ebooks.oeb.polish.split.split(container, name, loc_or_xpath, before=True, totals=None)[소스]

name으로 지정한 파일을 loc_or_xpath로 지정한 위치에서 분할합니다. 분할 시 영향받는 파일에 대한 모든 링크와 참조가 자동으로 옮겨집니다.

매개변수:

- loc_or_xpath – //h:div[@id=”split_here”] 같은 XPath 식이어야 합니다. 미리보기 패널에서 분할 기능을 구현하기 위해 내부적으로 사용하는 loc 값도 사용할 수 있습니다.

- before – True이면 식별된 요소 앞에서 분할하고, 그렇지 않으면 뒤에서 분할합니다.

- totals – 내부적으로 사용됩니다.

calibre.ebooks.oeb.polish.split.multisplit(container, name, xpath, before=True)[소스]

지정한 파일을 여러 위치(지정한 XPath 식과 일치하는 모든 태그)에서 분할합니다. 참고: split(). 분할 시 영향받는 파일에 대한 모든 링크와 참조가 자동으로 옮겨집니다.

매개변수:

before – True이면 식별된 요소 앞에서 분할이 일어나고, 그렇지 않으면 뒤에서 분할됩니다.

calibre.ebooks.oeb.polish.split.merge(container, category, names, master)[소스]

지정한 파일들을 하나의 파일로 병합하고, 영향받는 파일에 대한 모든 링크와 참조를 자동으로 옮깁니다. 파일은 모두 HTML 파일이거나 모두 CSS 파일이어야 합니다.

매개변수:

- category – HTML 파일이면 'text', CSS 파일이면 'styles' 이어야 합니다.

- names – 병합할 파일 목록

- master – 병합된 파일 가운데 마스터 파일이 될 파일입니다. 즉, 병합 후에도 남아 있을 파일입니다.

표지 관리

calibre.ebooks.oeb.polish.cover.set_cover(container, cover_path, report=None, options=None)[소스]

책의 표지를 cover_path가 가리키는 이미지로 설정합니다.

매개변수:

- cover_path – 이미지 파일의 절대 경로이거나 책 안에 있는 이미지의 정규 이름입니다. 책 안의 이미지를 사용할 때는 아래 설명처럼 options도 설정해야 합니다.

- report – 단일 인수를 받는 선택적 호출 가능 객체입니다. 처리 중인 작업에 대한 정보가 이 객체로 전달됩니다.

- options – 표지 설정 방식을 제어하는 None 또는 사전입니다. 사전에는 다음 항목이 들어갈 수 있습니다. keep_aspect: True 또는 False (EPUB에서 표지의 종횡비 유지) no_svg: True 또는 False (EPUB 제목 페이지에서 SVG 표지 래퍼 사용) existing: True 또는 False (``cover_path``가 책 안의 기존 이미지를 가리킴)

calibre.ebooks.oeb.polish.cover.mark_as_cover(container, name)[소스]

지정한 이미지를 표지 이미지로 표시합니다.

calibre.ebooks.oeb.polish.cover.mark_as_titlepage(container, name, move_to_start=True)[소스]

지정한 HTML 파일을 EPUB의 제목 페이지로 표시합니다.

매개변수:

move_to_start – True이면 해당 HTML 파일을 spine의 시작 위치로 이동합니다.

CSS 다루기

calibre.ebooks.oeb.polish.fonts.change_font(container, old_name, new_name=None)[소스]

글꼴 패밀리를 old_name에서 new_name으로 바꿉니다. 스타일시트, style 태그, style 속성에 있는 해당 글꼴 패밀리의 모든 항목이 변경됩니다. old_name이 내장 글꼴을 가리키면 그 글꼴은 제거됩니다. 글꼴 패밀리를 변경하는 대신 제거하려면 new_name에 None을 지정하면 됩니다.

calibre.ebooks.oeb.polish.css.remove_unused_css(container, report=None, remove_unused_classes=False, merge_rules=False, merge_rules_with_identical_properties=False, remove_unreferenced_sheets=False)[소스]

책에서 사용되지 않는 CSS 규칙을 모두 제거합니다. 사용되지 않는 CSS 규칙은 실제 콘텐츠와 일치하지 않는 규칙입니다.

매개변수:

- report – 단일 인수를 받는 선택적 호출 가능 객체입니다. 수행 중인 작업에 관한 정보가 이 객체로 전달됩니다.

- remove_unused_classes – True이면 어떤 CSS 규칙과도 일치하지 않는 HTML의 class 속성도 제거합니다.

- merge_rules – True이면 동일한 선택자를 가진 규칙을 병합합니다.

- merge_rules_with_identical_properties – True이면 동일한 속성을 가진 규칙을 병합합니다.

- remove_unreferenced_sheets – True이면 어떤 콘텐츠에서도 참조되지 않는 스타일시트를 제거합니다.

calibre.ebooks.oeb.polish.css.filter_css(container, properties, names=())[소스]

책의 모든 CSS 규칙에서 지정한 CSS 속성을 제거합니다.

매개변수:

- properties – 제거할 속성 집합입니다. 예: {'font-family', 'color'}

- names – 속성을 제거할 파일입니다. 기본값은 책 안의 모든 HTML 및 CSS 파일입니다.

목차 다루기

calibre.ebooks.oeb.polish.toc.from_xpaths(container, xpaths, prefer_title=False)[소스]

XPath 식 목록에서 목차를 생성합니다. 목록의 각 식은 생성되는 목차의 한 수준에 해당합니다. 예를 들어 ['//h:h1', '//h:h2', '//h:h3'] 는 <h1>, <h2>, <h3> 태그로 3단계 목차를 생성합니다.

calibre.ebooks.oeb.polish.toc.from_links(container)[소스]

책 안의 링크에서 목차를 생성합니다.

calibre.ebooks.oeb.polish.toc.from_files(container)[소스]

책 안의 파일에서 목차를 생성합니다.

calibre.ebooks.oeb.polish.toc.create_inline_toc(container, title=None)[소스]

기존 NCX 목차로부터 인라인(HTML) 목차를 생성합니다.

매개변수:

title – 이 목차의 제목입니다.

책 편집 도구
class calibre.gui2.tweak_book.plugin.Tool[소스]

기반 클래스: object

Edit Book 플러그인에서 개별 도구의 기본 클래스입니다. 유용한 멤버는 다음과 같습니다:

- self.plugin: 이 도구가 속한 calibre.customize.Plugin 객체에 대한 참조입니다.

- self. boss

- self. gui

하위 클래스에서 반드시 재정의해야 하는 메서드:

- create_action()

- register_shortcut()

name = None

고유한 이름으로 설정하십시오. 키로 사용됩니다.

allowed_in_toolbar = True

True이면 사용자가 이 도구를 플러그인 도구 모음에 배치할 수 있습니다.

allowed_in_menu = True

True이면 사용자가 이 도구를 플러그인 메뉴에 배치할 수 있습니다.

toolbar_button_popup_mode = 'delayed'

도구 모음 버튼 메뉴(있는 경우)의 팝업 모드입니다. 가능한 값은 ‘delayed’, ‘instant’, ‘button’입니다.

property boss

사용자 인터페이스를 제어하는 calibre.gui2.tweak_book.boss.Boss 객체입니다.

property gui

사용자 인터페이스의 메인 창

property current_container

현재 편집 중인 책을 나타내는 calibre.ebooks.oeb.polish.container.Container 객체를 반환합니다.

register_shortcut(qaction, unique_name, default_keys=(), short_text=None, description=None, **extra_data)[소스]

지정한 ``qaction``을 실행할 키보드 단축키를 등록합니다. 이 단축키는 편집기 환경설정의 키보드 단축키 섹션에서 사용자가 자동으로 사용자 지정할 수 있게 됩니다.

매개변수:

- qaction – QAction 객체입니다. 사용자가 설정된 키 조합을 누르면 이 객체가 실행됩니다.

- unique_name – 이 단축키/동작의 고유 이름입니다. 내부적으로 사용되며, 이 플러그인의 다른 어떤 동작과도 중복되면 안 됩니다.

- default_keys – 기본 키보드 단축키 목록입니다. 지정하지 않으면 기본 단축키는 설정되지 않습니다. 여기 지정한 단축키가 내장 단축키 또는 사용자 설정/다른 플러그인의 단축키와 충돌하면 무시됩니다. 그런 경우 사용자는 환경설정에서 직접 단축키를 설정해야 합니다. 예: default_keys=('Ctrl+J', 'F9').

- short_text – 이 동작의 선택적 짧은 설명입니다. 지정하지 않으면 QAction의 텍스트가 사용됩니다.

- description – 이 동작의 선택적 긴 설명입니다. 이 단축키의 환경설정 항목에 사용됩니다.

create_action(for_toolbar=True)[소스]

for_toolbar 값에 따라 플러그인 도구 모음 또는 플러그인 메뉴에 추가될 QAction을 생성합니다. 예:

def create_action(self, for_toolbar=True):
ac = QAction(get_icons('myicon.png'), 'Do something')
if for_toolbar:
# We want the toolbar button to have a popup menu
menu = QMenu()
ac.setMenu(menu)
menu.addAction('Do something else')
subaction = menu.addAction('And another')

# Register a keyboard shortcut for this toolbar action be
# careful to do this for only one of the toolbar action or
# the menu action, not both.
self.register_shortcut(ac, 'some-unique-name', default_keys=('Ctrl+K',))
return ac

더 보기

메서드 register_shortcut().

편집기 사용자 인터페이스 제어

전자책 편집기의 사용자 인터페이스는 전역 Boss 객체 하나로 제어됩니다. 이 객체에는 플러그인 코드에서 일반 작업을 수행할 때 사용할 수 있는 유용한 메서드가 많이 있습니다.

class calibre.gui2.tweak_book.boss.Boss(parent, notify=None)[소스]

add_savepoint(msg)[소스]

``msg``로 지정한 이름의 복원 체크포인트를 생성합니다.

apply_container_update_to_gui(mark_as_modified=True)[소스]

현재 책 컨테이너의 최신 데이터가 반영되도록 사용자 인터페이스의 모든 구성 요소를 갱신합니다.

매개변수:

mark_as_modified – True이면 책이 수정된 것으로 표시되어, 종료할 때 사용자에게 저장할지 묻습니다.

close_editor(name)[소스]

``name``으로 지정한 파일을 편집 중인 편집기를 닫습니다.

commit_all_editors_to_container()[소스]

편집기에서 열려 있는 파일에 사용자가 가한 변경 사항을 컨테이너에 커밋합니다. 현재 컨테이너에 대해 작업을 수행하기 전에 이 메서드를 호출해야 합니다.

property currently_editing

현재 편집 중인 파일의 이름을 반환합니다. 편집 중인 파일이 없으면 None을 반환합니다.

edit_file(name, syntax=None, use_template=None)[소스]

name으로 지정한 파일을 편집기에서 엽니다.

매개변수:

- syntax – 파일의 미디어 형식입니다. 예: 'text/html'. 지정하지 않으면 파일 확장자로 추정합니다.

- use_template – 열린 편집기를 초기화하는 데 사용할 템플릿입니다.

open_book(path=None, edit_file=None, clear_notify_data=True, open_folder=False, search_text=None)[소스]

``path``에 있는 전자책을 편집용으로 엽니다. 전자책 형식이 지원되지 않거나 현재 책에 저장되지 않은 변경 사항이 있으면 오류를 표시합니다.

매개변수:

edit_file – 새로 연 책 안에서 처음 편집을 시작할 파일 이름입니다. 이름 목록을 지정할 수도 있습니다.

rewind_savepoint()[소스]

이전에 만든 복원 체크포인트를 취소합니다. 체크포인트를 만든 뒤 아무 변경 없이 작업을 중단할 때 유용합니다.

save_book()[소스]

책을 저장합니다. 저장은 백그라운드에서 수행됩니다.

set_modified()[소스]

책이 수정된 것으로 표시합니다.

show_current_diff(allow_revert=True, to_container=None)[소스]

마지막 체크포인트 상태와 비교한 책의 변경 내용을 표시합니다.

매개변수:

- allow_revert – True이면 diff 대화 상자에 모든 변경 내용을 되돌릴 수 있는 버튼이 표시됩니다.

- to_container – 현재 컨테이너와 비교할 컨테이너 객체입니다. None이면 이전에 체크포인트된 컨테이너를 사용합니다.

show_editor(name)[소스]

``name``으로 지정한 파일을 편집 중인 편집기를 표시합니다.

sync_preview_to_editor()[소스]

미리보기 패널의 위치를 현재 편집기의 현재 커서 위치와 동기화합니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 다음 |
- 이전 |
- 시작 »
- 디지털 저작권 관리 (DRM)

디지털 저작권 관리 (DRM)

디지털 저작권 관리(DRM)는 하드웨어 제조사, 출판사, 저작권 보유자 및 개인이 디지털 콘텐츠와 기기의 사용에 제한을 부과하기 위해 사용할 수 있는 접근 제어 기술의 일반적인 용어입니다. 때때로 경멸적으로 디지털 제한 관리(Digital Restrictions Management)라고도 합니다. 이 용어는 콘텐츠 제공자가 원하지 않거나 예상하지 못한 디지털 콘텐츠의 사용(합법적이든 아니든)을 억제하는 기술을 지칭합니다. 이 용어는 일반적으로 파일이나 기기를 수정하지 않고 우회할 수 있는 시리얼 번호나 키 파일 같은 다른 형태의 복사 보호를 가리키지 않습니다. 또한 디지털 작품이나 기기의 특정 인스턴스와 관련된 제한을 가리킬 수도 있습니다. DRM 기술은 최종 사용자의 접근, 복사 또는 다른 형식으로의 변환을 방지하여 디지털 미디어의 사용을 제어하려고 시도합니다. `위키피디아 <https://en.wikipedia.org/wiki/Digital_rights_management>`_를 참조하세요.

DRM은 개인적으로 무엇을 의미하나요?

DRM이 포함된 전자책을 구매할 때 실제로 소유하는 것이 아니라 판매자가 지시한 방식으로 사용할 수 있는 권한을 구매하는 것입니다. DRM은 “구매한” 전자책으로 할 수 있는 것을 제한합니다. DRM이 포함된 책을 구매하는 사람들은 종종 이러한 제한의 범위를 인식하지 못합니다. 이러한 제한은 글꼴 크기 조정과 같은 스타일 변경을 포함하여 전자책을 원하는 대로 재서식화하는 것을 방해합니다. DRM이 없는 책의 경우 이러한 작업을 수행할 수 있는 소프트웨어가 있지만 말입니다. 사람들은 종종 특정 형식으로 구매한 전자책이 DRM이 포함된 경우 다른 형식으로 변환할 수 없다는 사실에 놀라곤 합니다. 따라서 Amazon Kindle을 가지고 있고 Barnes and Nobles에서 판매하는 책을 구매한 경우, 해당 전자책에 DRM이 있으면 Kindle에서 읽을 수 없다는 것을 알아야 합니다. 여기서 말하는 것은 훔치거나 불법 복제하는 것이 아니라 구매하는 책에 관한 것입니다.

DRM은 저자에게 무엇을 해주나요?

DRM이 적용된 전자책의 출판사들은 DRM이 전적으로 저자를 위한 것이며 그들의 예술적 완전성을 보호하고 불법 복제를 방지하기 위한 것이라고 주장합니다. 그러나 DRM은 불법 복제를 방지하지 못합니다. 콘텐츠를 불법 복제하거나 불법 복제된 콘텐츠를 사용하고 싶어하는 사람들은 여전히 그렇게 하며 성공합니다. 오늘날 전자책의 세 가지 주요 DRM 방식은 Amazon, Adobe, Barnes and Noble이 운영하며 세 가지 DRM 방식 모두 이미破解되었습니다. DRM이 하는 일은 합법적인 사용자를 불편하게 만드는 것뿐입니다. DRM을 참을 수 없어 책을 구매하려던 사람들이 불법 복제 버전을 찾도록 선택하므로 실제로 저자를 해칠 수도 있다고 주장할 수 있습니다. DRM이 없는 경우 불법 복제를 할 사람들은 DRM이 있는 경우에도 그렇게 합니다. 다시 말하지만 핵심은 DRM이 불법 복제를 방지하지 못한다는 것입니다. 따라서 DRM은 전자책 구매자에게 무의미하고 해로울 뿐만 아니라 돈 낭비이기도 합니다.

DRM과 자유

디지털 콘텐츠를 사용하여 정보와 창작물을 모든 사람이 쉽게 이용할 수 있게 하고 인류에 힘을 실어줄 수 있지만, 이는 너무 빠르게 발전하는 세상에서 자신의 관련성을 유지하기 위해 단지 이러한 자유의 가능성을 사람들의 시야에서 벗어나게 하려는 일부 출판사의 이익에 부합하지 않습니다.

캘리버는 왜 DRM을 지원하지 않나요?

캘리버는 오픈 소스 소프트웨어인 반면 DRM은 그 본질상 폐쇄적입니다. 캘리버가 DRM 파일의 열기나 보기를 지원하면 오늘날 법률에 따라 불법인 DRM 제거 도구로 간단하게 수정될 수 있습니다. 오픈 소스 소프트웨어와 DRM은 원칙의 충돌입니다. DRM은 사용자를 제어하는 것에 관한 것이고, 오픈 소스 소프트웨어는 사용자에게 힘을 실어주는 것에 관한 것입니다. 이 둘은 단순히 공존할 수 없습니다.

콘텐츠 제공자에 대한 캘리버의 견해는 무엇인가요?

저자와 기타 콘텐츠 제공자는 그들의 노력에 대한 보상을 받아야 한다고 굳게 믿지만, DRM이 그 방법은 아닙니다. 저희는 다양한 소스의 DRM-free 전자책 데이터베이스를 개발하여 DRM-free 대안을 찾는 것을 돕고, DRM-free 전자책을 출판하는 독립 저자와 출판사가 자신의 콘텐츠를 홍보하는 것을 돕고 있습니다. 이것이 유용하기를 바라며, 여기에서 제공되는 콘텐츠를 불법 복제하지 않기를 부탁드립니다.

DRM에 맞서 싸우려면 어떻게 해야 하나요?

전자책을 읽고 구매하는 사람으로서 DRM에 맞서 싸우는 데 도움을 줄 수 있습니다. DRM이 포함된 전자책을 구매하지 마세요. DRM-free 전자책을 출판하는 출판사가 있습니다. 찾고 있는 전자책을 보유하고 있는지 확인하기 위해 노력하세요. DRM-free 전자책을 판매하는 특정 독립 저자의 책을 좋아하고 여유가 있다면 후원하세요. 그들의 전자책은 DRM이 적용된 책의 출판사에서 구매하는 것보다 저렴한 경향이(예외가 있을 수 있음) 있고 미래에 소유하는 모든 기기에서 작동할 것이므로 전자책을 다시 구매하는 비용을 절약할 수 있으므로 이는 돈을 잘 쓰는 것입니다. DRM-free 전자책의 출판사와 저자를 불법 복제로 좌절시키지 마세요. 콘텐츠 제공자는 노력에 대한 보상을 받을 자격이 있습니다. DRM-free 전자책을 제공하여 독서 경험을 더 좋게 만들려는 그들을 처벌하지 마세요. 장기적으로 이것은 여러분에게 해롭습니다. DRM이 적용된 책과 DRM-free 책을 모두 취급하는 판매자에게서 책을 구매했는데 DRM 포함 여부를 모르는 경우, 웹사이트에 댓글이나 리뷰를 남겨 향후 구매자에게 DRM 상태를 알리는 것이 중요합니다. 많은 판매자는 전자책에 DRM이 포함되어 있는지 여부를 구매자에게 명확히 표시하는 것이 중요하다고 생각하지 않습니다. `여기 <https://www.defectivebydesign.org/guide/ebooks>`_에서 DRM-free 생활 가이드를 찾을 수 있습니다.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 이전 |
- 시작 »
- 용어집

용어집

RSS

RSS (Really Simple Syndication)*은 뉴스 기사, 블로그 게시물 등과 같이 자주 업데이트되는 콘텐츠를 게시하는 데 사용되는 웹 피드 형식입니다. 특히 컴퓨터에서 읽기에 적합한 형식이며, 따라서 웹에서 전자책으로 콘텐츠를 가져오는 데 선호되는 방법입니다. 인터넷에는 다른 많은 피드 형식이 사용 중이며 캘리버는 대부분을 이해하고 있습니다. 특히 블로그에서 많이 사용하는 *ATOM 형식을 잘 지원합니다.

레시피

레시피는 잡지나 블로그와 같은 온라인 뉴스 소스를 전자책으로 변환하는 방법을 알려주는 일련의 지침입니다. 레시피는 본질적으로 Python [https://www.python.org] 코드입니다. 이처럼 임의로 복잡한 뉴스 소스를 전자책으로 변환할 수 있습니다. 가장 단순한 수준에서, 그것은 단지 URL과 같은 변수 집합으로, 인터넷에 나가서 뉴스를 다운로드 할 수 있는 충분한 정보를 제공합니다.

HTML

**HTML**은 전자 출판을 위한 Standard Generalized Mark-Up Language (SGML)의 부분집합이며, 월드 와이드 웹(WWW)을 위한 표준입니다.

CSS

CSS *(Cascading Style Sheets)*는 HTML 문서가 어떻게 렌더링(시각적 형태) 되어야 하는지를 기술하는 언어입니다.

API

API *(Application Programming Interface)*는 컴퓨터 프로그램의 서비스를 사용하기위한 요청을 지원하기 위해 라이브러리가 제공하는 소스코드 인터페이스입니다.

LRF

LRF SONY 전자책 독자가 읽는 전자책 형식입니다.

URL

URL (Uniform Resource Locator), 예) http://example.com

regexp

**정규 표현식**은 특정 문자, 단어 또는 문자 패턴과 같은 관심 있는 텍스트 문자열을 식별하는 간결하고 유연한 방법을 제공합니다. 정규 표현식에 대한 소개는 :doc:`튜토리얼 <regexp>`을 참조하세요.

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 시작 »
- Python 모듈 목록

Python 모듈 목록
c

| c |
| calibre |
| calibre.customize | Defines various abstract base classes that can be subclassed to create plugins.
| calibre.customize.conversion |
| calibre.db.cache | The API for accessing and manipulating a calibre library.
| calibre.devices.interface |
| calibre.ebooks.metadata.book.base |
| calibre.ebooks.metadata.sources.base |
| calibre.ebooks.oeb.polish.container | The container object used to represent a book as a collection of its constituent HTML files.
| calibre.ebooks.oeb.polish.cover |
| calibre.ebooks.oeb.polish.css |
| calibre.ebooks.oeb.polish.jacket |
| calibre.ebooks.oeb.polish.pretty |
| calibre.ebooks.oeb.polish.replace |
| calibre.ebooks.oeb.polish.split |
| calibre.ebooks.oeb.polish.toc |
| calibre.gui2.tweak_book.boss |
| calibre.utils.formatter_functions |
| calibre.web.feeds.news | The API for writing recipes is defined by the :class:`BasicNewsRecipe`

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 시작 »
- 개요: 모듈 코드

코드를 확인할 수 있는 모든 모듈

- PyQt6.QtCore
- PyQt6.QtWidgets
- calibre.customize

- calibre.customize.conversion

- calibre.db.cache
- calibre.devices.interface
- calibre.devices.usbms.cli
- calibre.devices.usbms.device
- calibre.devices.usbms.driver
- calibre.ebooks.metadata.book.base
- calibre.ebooks.metadata.sources.base
- calibre.ebooks.oeb.polish.container
- calibre.ebooks.oeb.polish.cover
- calibre.ebooks.oeb.polish.css
- calibre.ebooks.oeb.polish.fonts
- calibre.ebooks.oeb.polish.jacket
- calibre.ebooks.oeb.polish.pretty
- calibre.ebooks.oeb.polish.replace
- calibre.ebooks.oeb.polish.split
- calibre.ebooks.oeb.polish.toc
- calibre.gui2.actions
- calibre.gui2.preferences
- calibre.gui2.tweak_book.boss
- calibre.gui2.tweak_book.plugin
- calibre.utils.formatter_functions
- calibre.web.feeds.news

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 시작 »
- 모듈 코드 »
- calibre.customize

calibre.customize의 소스 코드

__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'

import enum import importlib import os import sys import zipfile from typing import TYPE_CHECKING from calibre.constants import ismacos, iswindows, numeric_version from calibre.ptempfile import PersistentTemporaryFile if TYPE_CHECKING:
from collections.abc import Iterable, Iterator from calibre.ai import ChatMessage, ChatResponse if iswindows:
platform = 'windows'
elif ismacos:
platform = 'osx'
else:
platform = 'linux'

class PluginNotFound(ValueError):
pass class InvalidPlugin(ValueError):
pass class PluginInstallationType(enum.IntEnum):
EXTERNAL = 1
SYSTEM = 2
BUILTIN = 3

[문서]
class Plugin: # {{{
'''
A calibre plugin. Useful members include:

* ``self.installation_type``: Stores how the plugin was installed.
* ``self.plugin_path``: Stores path to the ZIP file that contains this plugin or None if it is a builtin plugin
* ``self.site_customization``: Stores a customization string entered by the user.

Methods that should be overridden in sub classes:

* :meth:`initialize`
* :meth:`customization_help`

Useful methods:

* :meth:`temporary_file`
* :meth:`__enter__`
* :meth:`load_resources`

'''
#: List of platforms this plugin works on.
#: For example: ``['windows', 'osx', 'linux']`` supported_platforms = []

#: The name of this plugin. You must set it something other
#: than Trivial Plugin for it to work.
name = 'Trivial Plugin'

#: The version of this plugin as a 3-tuple (major, minor, revision)
version = (1, 0, 0)

#: A short string describing what this plugin does description = _('Does absolutely nothing')

#: The author of this plugin author = _('Unknown')

#: When more than one plugin exists for a filetype,
#: the plugins are run in order of decreasing priority.
#: Plugins with higher priority will be run first.
#: The highest possible priority is ``sys.maxsize``.
#: Default priority is 1.
priority = 1

#: The earliest version of calibre this plugin requires minimum_calibre_version = (0, 4, 118)

#: The way this plugin is installed installation_type = None

#: If False, the user will not be able to disable this plugin. Use with
#: care.
can_be_disabled = True

#: The type of this plugin. Used for categorizing plugins in the
#: GUI type = _('Base')

def __init__(self, plugin_path):
self.plugin_path = plugin_path self.site_customization = None

[문서]
def initialize(self):
'''
Called once when calibre plugins are initialized. Plugins are re-initialized every time a new plugin is added. Also note that if the plugin is run in a worker process, such as for adding books, then the plugin will be initialized for every new worker process.

Perform any plugin specific initialization here, such as extracting resources from the plugin ZIP file. The path to the ZIP file is available as ``self.plugin_path``.

Note that ``self.site_customization`` is **not** available at this point.
'''
pass

[문서]
def config_widget(self):
'''
Implement this method and :meth:`save_settings` in your plugin to use a custom configuration dialog, rather then relying on the simple string based default customization.

This method, if implemented, must return a QWidget. The widget can have an optional method validate() that takes no arguments and is called immediately after the user clicks OK. Changes are applied if and only if the method returns True.

If for some reason you cannot perform the configuration at this time, return a tuple of two strings (message, details), these will be displayed as a warning dialog to the user and the process will be aborted.
'''
raise NotImplementedError()

[문서]
def save_settings(self, config_widget):
'''
Save the settings specified by the user with config_widget.

:param config_widget: The widget returned by :meth:`config_widget`.

'''
raise NotImplementedError()

[문서]
def do_user_config(self, parent=None):
'''
This method shows a configuration dialog for this plugin. It returns
True if the user clicks OK, False otherwise. The changes are automatically applied.
'''
from qt.core import QApplication, QDialog, QDialogButtonBox, QLabel, QLineEdit, QScrollArea, QSize, Qt, QVBoxLayout from calibre.gui2 import gprefs class ConfigDialog(QDialog):

def __init__(self, parent, config_widget):
super().__init__(parent)
self.config_widget = config_widget def accept(self):
if ((validate := getattr(self.config_widget, 'validate', None)) and getattr(self.config_widget, 'validate_before_accept', False)):
if not validate():
return super().accept()

try:
config_widget = self.config_widget()
except NotImplementedError:
config_widget = None prefname = 'plugin config dialog:'+self.type + ':' + self.name config_dialog = ConfigDialog(parent, config_widget)
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
v = QVBoxLayout(config_dialog)
button_box.accepted.connect(config_dialog.accept)
button_box.rejected.connect(config_dialog.reject)
config_dialog.setWindowTitle(_('Customize') + ' ' + self.name)

if isinstance(config_widget, tuple):
from calibre.gui2 import warning_dialog warning_dialog(parent, _('Cannot configure'), config_widget[0], det_msg=config_widget[1], show=True)
return False if config_widget is not None:
class SA(QScrollArea):
def sizeHint(self):
sz = self.widget().sizeHint()
fw = 2 * self.frameWidth()
return QSize(sz.width() + self.verticalScrollBar().sizeHint().width() + fw, sz.height() + fw)
sa = SA(config_dialog)
sa.setWidget(config_widget)
sa.setWidgetResizable(True)
v.addWidget(sa)
v.addWidget(button_box)
if not config_dialog.restore_geometry(gprefs, prefname):
QApplication.instance().ensure_window_on_screen(config_dialog)
config_dialog.exec()

if config_dialog.result() == QDialog.DialogCode.Accepted:
if hasattr(config_widget, 'validate'):
if config_widget.validate():
self.save_settings(config_widget)
else:
self.save_settings(config_widget)
else:
from calibre.customize.ui import customize_plugin, plugin_customization help_text = self.customization_help(gui=True)
help_text = QLabel(help_text, config_dialog)
help_text.setWordWrap(True)
help_text.setTextInteractionFlags(Qt.TextInteractionFlag.LinksAccessibleByMouse | Qt.TextInteractionFlag.LinksAccessibleByKeyboard)
help_text.setOpenExternalLinks(True)
v.addWidget(help_text)
sc = plugin_customization(self)
if not sc:
sc = ''
sc = sc.strip()
sc = QLineEdit(sc, config_dialog)
v.addWidget(sc)
v.addWidget(button_box)
config_dialog.restore_geometry(gprefs, prefname)
config_dialog.exec()

if config_dialog.result() == QDialog.DialogCode.Accepted:
sc = str(sc.text()).strip()
customize_plugin(self, sc)

config_dialog.save_geometry(gprefs, prefname)
return config_dialog.result()

[문서]
def load_resources(self, names):
'''
If this plugin comes in a ZIP file (user added plugin), this method will allow you to load resources from the ZIP file.

For example to load an image::

pixmap = QPixmap()
pixmap.loadFromData(self.load_resources(['images/icon.png'])['images/icon.png'])
icon = QIcon(pixmap)

:param names: List of paths to resources in the ZIP file using / as separator

:return: A dictionary of the form ``{name: file_contents}``. Any names that were not found in the ZIP file will not be present in the dictionary.

'''
if self.plugin_path is None:
raise ValueError('This plugin was not loaded from a ZIP file')
ans = {}
with zipfile.ZipFile(self.plugin_path, 'r') as zf:
for candidate in zf.namelist():
if candidate in names:
ans[candidate] = zf.read(candidate)
return ans

[문서]
def customization_help(self, gui=False):
'''
Return a string giving help on how to customize this plugin.
By default raise a :class:`NotImplementedError`, which indicates that the plugin does not require customization.

If you re-implement this method in your subclass, the user will be asked to enter a string as customization for this plugin.
The customization string will be available as
``self.site_customization``.

Site customization could be anything, for example, the path to a needed binary on the user's computer.

:param gui: If True return HTML help, otherwise return plain text help.

'''
raise NotImplementedError()

[문서]
def temporary_file(self, suffix):
'''
Return a file-like object that is a temporary file on the file system.
This file will remain available even after being closed and will only be removed on interpreter shutdown. Use the ``name`` member of the returned object to access the full path to the created temporary file.

:param suffix: The suffix that the temporary file will have.
'''
return PersistentTemporaryFile(suffix)

def is_customizable(self):
try:
self.customization_help()
return True except NotImplementedError:
return False def __enter__(self, *args):
'''
Add this plugin to the python path so that it's contents become directly importable.
Useful when bundling large python libraries into the plugin. Use it like this::
with plugin:
import something
'''
if self.plugin_path is not None:
from importlib.machinery import EXTENSION_SUFFIXES from calibre.utils.zipfile import ZipFile with ZipFile(self.plugin_path) as zf:
extensions = {x.lower() for x in EXTENSION_SUFFIXES}
zip_safe = True for name in zf.namelist():
for q in extensions:
if name.endswith(q):
zip_safe = False break if not zip_safe:
break if zip_safe:
sys.path.append(self.plugin_path)
self.sys_insertion_path = self.plugin_path else:
from calibre.ptempfile import TemporaryDirectory self._sys_insertion_tdir = TemporaryDirectory('plugin_unzip')
self.sys_insertion_path = self._sys_insertion_tdir.__enter__(*args)
zf.extractall(self.sys_insertion_path)
sys.path.append(self.sys_insertion_path)

def __exit__(self, *args):
ip, it = getattr(self, 'sys_insertion_path', None), getattr(self,
'_sys_insertion_tdir', None)
if ip in sys.path:
sys.path.remove(ip)
if hasattr(it, '__exit__'):
it.__exit__(*args)

[문서]
def cli_main(self, args):
'''
This method is the main entry point for your plugins command line interface. It is called when the user does: calibre-debug -r "Plugin
Name". Any arguments passed are present in the args variable.
'''
raise NotImplementedError(f'The {self.name} plugin has no command line interface')

# }}}

[문서]
class FileTypePlugin(Plugin): # {{{
'''
A plugin that is associated with a particular set of file types.
'''

#: Set of file types for which this plugin should be run.
#: Use '*' for all file types.
#: For example: ``{'lit', 'mobi', 'prc'}`` file_types = set()

#: If True, this plugin is run when books are added
#: to the database on_import = False

#: If True, this plugin is run after books are added
#: to the database. In this case the postimport and postadd
#: methods of the plugin are called.
on_postimport = False

#: If True, this plugin is run after a book is converted.
#: In this case the postconvert method of the plugin is called.
on_postconvert = False

#: If True, this plugin is run after a book file is deleted
#: from the database. In this case the postdelete method of
#: the plugin is called.
on_postdelete = False

#: If True, this plugin is run just before a conversion on_preprocess = False

#: If True, this plugin is run after conversion
#: on the final file produced by the conversion output plugin.
on_postprocess = False type = _('File type')

[문서]
def run(self, path_to_ebook):
'''
Run the plugin. Must be implemented in subclasses.
It should perform whatever modifications are required on the e-book and return the absolute path to the modified e-book. If no modifications are needed, it should return the path to the original e-book. If an error is encountered it should raise an Exception. The default implementation simply return the path to the original e-book. Note that the path to the original file (before any file type plugins are run, is available as self.original_path_to_file).

The modified e-book file should be created with the
:meth:`temporary_file` method.

:param path_to_ebook: Absolute path to the e-book.

:return: Absolute path to the modified e-book.
'''
# Default implementation does nothing return path_to_ebook

[문서]
def postimport(self, book_id, book_format, db):
'''
Called post import, i.e., after the book file has been added to the database. Note that this is different from :meth:`postadd` which is called when the book record is created for the first time. This method is called whenever a new file is added to a book record. It is useful for modifying the book record based on the contents of the newly added file.

:param book_id: Database id of the added book.
:param book_format: The file type of the book that was added.
:param db: Library database.
'''
pass # Default implementation does nothing

[문서]
def postconvert(self, book_id, book_format, db):
'''
Called post conversion, i.e., after the conversion output book file has been added to the database.
Note that it is run after a conversion only, not after a book is added. It is useful for modifying the book record based on the contents of the newly added file.

:param book_id: Database id of the added book.
:param book_format: The file type of the book that was added.
:param db: Library database.
'''
pass # Default implementation does nothing

[문서]
def postdelete(self, book_id, book_format, db):
'''
Called post deletion, i.e., after the book file has been deleted from the database. Note that it is not run when a book record is deleted, only when one or more formats from the book are deleted. It is useful for modifying the book record based on the format of the deleted file.

:param book_id: Database id of the added book.
:param book_format: The file type of the book that was added.
:param db: Library database.
'''
pass # Default implementation does nothing

[문서]
def postadd(self, book_id, fmt_map, db):
'''
Called post add, i.e. after a book has been added to the db. Note that this is different from :meth:`postimport`, which is called after a single book file has been added to a book. postadd() is called only when an entire book record with possibly more than one book file has been created for the first time.
This is useful if you wish to modify the book record in the database when the book is first added to calibre.

:param book_id: Database id of the added book.
:param fmt_map: Map of file format to path from which the file format was added. Note that this might or might not point to an actual existing file, as sometimes files are added as streams. In which case it might be a dummy value or a non-existent path.
:param db: Library database
'''
pass # Default implementation does nothing

# }}}

[문서]
class MetadataReaderPlugin(Plugin): # {{{
'''
A plugin that implements reading metadata from a set of file types.
'''
#: Set of file types for which this plugin should be run.
#: For example: ``set(['lit', 'mobi', 'prc'])`` file_types = set()

supported_platforms = ['windows', 'osx', 'linux']
version = numeric_version author = 'Kovid Goyal'

type = _('Metadata reader')

def __init__(self, *args, **kwargs):
Plugin.__init__(self, *args, **kwargs)
self.quick = False

[문서]
def get_metadata(self, stream, type):
'''
Return metadata for the file represented by stream (a file like object that supports reading). Raise an exception when there is an error with the input data.

:param type: The type of file. Guaranteed to be one of the entries in :attr:`file_types`.
:return: A :class:`calibre.ebooks.metadata.book.Metadata` object
'''
return

# }}}

[문서]
class MetadataWriterPlugin(Plugin): # {{{
'''
A plugin that implements reading metadata from a set of file types.
'''
#: Set of file types for which this plugin should be run.
#: For example: ``set(['lit', 'mobi', 'prc'])`` file_types = set()

supported_platforms = ['windows', 'osx', 'linux']
version = numeric_version author = 'Kovid Goyal'

type = _('Metadata writer')

def __init__(self, *args, **kwargs):
Plugin.__init__(self, *args, **kwargs)
self.apply_null = False

[문서]
def set_metadata(self, stream, mi, type):
'''
Set metadata for the file represented by stream (a file like object that supports reading). Raise an exception when there is an error with the input data.

:param type: The type of file. Guaranteed to be one of the entries in :attr:`file_types`.
:param mi: A :class:`calibre.ebooks.metadata.book.Metadata` object
'''
pass

# }}}

[문서]
class CatalogPlugin(Plugin): # {{{
'''
A plugin that implements a catalog generator.
'''

resources_path = None

#: Output file type for which this plugin should be run.
#: For example: 'epub' or 'xml'
file_types = set()

type = _('Catalog generator')

#: CLI parser options specific to this plugin, declared as `namedtuple` `Option`:
#:
#: from collections import namedtuple
#: Option = namedtuple('Option', 'option, default, dest, help')
#: cli_options = [Option('--catalog-title', default = 'My Catalog',
#: dest = 'catalog_title', help = (_('Title of generated catalog. \nDefault:') + " '" + '%default' + "'"))]
#: cli_options parsed in calibre.db.cli.cmd_catalog:option_parser()
#:
cli_options = []

def _field_sorter(self, key):
'''
Custom fields sort after standard fields
'''
if key.startswith('#'):
return f'~{key[1:]}'
else:
return key def search_sort_db(self, db, opts):

db.search(opts.search_text)

if getattr(opts, 'sort_by', None):
# 2nd arg = ascending db.sort(opts.sort_by, True)
return db.get_data_as_dict(ids=opts.ids)

def get_output_fields(self, db, opts):
# Return a list of requested fields all_std_fields = {'author_sort','authors','comments','cover','formats',
'id','isbn','library_name','ondevice','pubdate','publisher',
'rating','series_index','series','size','tags','timestamp',
'title_sort','title','uuid','languages','identifiers'}
all_custom_fields = set(db.custom_field_keys())
for field in list(all_custom_fields):
fm = db.field_metadata[field]
if fm['datatype'] == 'series':
all_custom_fields.add(field+'_index')
all_fields = all_std_fields.union(all_custom_fields)

if getattr(opts, 'fields', 'all') != 'all':
# Make a list from opts.fields of = [x.strip() for x in opts.fields.split(',')]
requested_fields = set(of)

# Validate requested_fields if requested_fields - all_fields:
from calibre.library import current_library_name invalid_fields = sorted(requested_fields - all_fields)
print('invalid --fields specified: {}'.format(', '.join(invalid_fields)))
print("available fields in '{}': {}".format(current_library_name(), ', '.join(sorted(all_fields))))
raise ValueError('unable to generate catalog with specified fields')

fields = [x for x in of if x in all_fields]
else:
fields = sorted(all_fields, key=self._field_sorter)

if not opts.connected_device['is_device_connected'] and 'ondevice' in fields:
fields.pop(int(fields.index('ondevice')))

return fields

[문서]
def initialize(self):
'''
If plugin is not a built-in, copy the plugin's .ui and .py files from the ZIP file to $TMPDIR.
Tab will be dynamically generated and added to the Catalog Options dialog in calibre.gui2.dialogs.catalog.py:Catalog
'''
from calibre.customize.builtins import plugins as builtin_plugins from calibre.customize.ui import config from calibre.ptempfile import PersistentTemporaryDirectory if type(self) not in builtin_plugins and self.name not in config['disabled_plugins']:
files_to_copy = [f'{self.name.lower()}.{ext}' for ext in ['ui','py']]
resources = zipfile.ZipFile(self.plugin_path,'r')

if self.resources_path is None:
self.resources_path = PersistentTemporaryDirectory('_plugin_resources', prefix='')

for file in files_to_copy:
try:
resources.extract(file, self.resources_path)
except Exception:
print(f' customize:__init__.initialize(): {file} not found in {os.path.basename(self.plugin_path)}')
continue resources.close()

[문서]
def run(self, path_to_output, opts, db, ids, notification=None):
'''
Run the plugin. Must be implemented in subclasses.
It should generate the catalog in the format specified in file_types, returning the absolute path to the generated catalog file. If an error is encountered it should raise an Exception.

The generated catalog file should be created with the
:meth:`temporary_file` method.

:param path_to_output: Absolute path to the generated catalog file.
:param opts: A dictionary of keyword arguments
:param db: A LibraryDatabase2 object
'''
# Default implementation does nothing raise NotImplementedError('CatalogPlugin.generate_catalog() default '
'method, should be overridden in subclass')

# }}}

[문서]
class InterfaceActionBase(Plugin): # {{{ supported_platforms = ['windows', 'osx', 'linux']
author = 'Kovid Goyal'
type = _('User interface action')

actual_plugin = None def __init__(self, *args, **kwargs):
Plugin.__init__(self, *args, **kwargs)
self.actual_plugin_ = None

[문서]
def load_actual_plugin(self, gui):
'''
This method must return the actual interface action plugin object.
'''
ac = self.actual_plugin_ if ac is None:
mod, cls = self.actual_plugin.split(':')
ac = getattr(importlib.import_module(mod), cls)(gui, self.site_customization)
self.actual_plugin_ = ac return ac

# }}}

[문서]
class PreferencesPlugin(Plugin): # {{{

'''
A plugin representing a widget displayed in the Preferences dialog.

This plugin has only one important method :meth:`create_widget`. The various fields of the plugin control how it is categorized in the UI.
'''

supported_platforms = ['windows', 'osx', 'linux']
author = 'Kovid Goyal'
type = _('Preferences')

#: Import path to module that contains a class named ConfigWidget
#: which implements the ConfigWidgetInterface. Used by
#: :meth:`create_widget`.
config_widget = None

#: Where in the list of categories the :attr:`category` of this plugin should be.
category_order = 100

#: Where in the list of names in a category, the :attr:`gui_name` of this
#: plugin should be name_order = 100

#: The category this plugin should be in category = None

#: The category name displayed to the user for this plugin gui_category = None

#: The name displayed to the user for this plugin gui_name = None

#: The icon for this plugin, should be an absolute path icon = None

#: The description used for tooltips and the like description = None

[문서]
def create_widget(self, parent=None):
'''
Create and return the actual Qt widget used for setting this group of preferences. The widget must implement the
:class:`calibre.gui2.preferences.ConfigWidgetInterface`.

The default implementation uses :attr:`config_widget` to instantiate the widget.
'''
base, _, wc = self.config_widget.partition(':')
if not wc:
wc = 'ConfigWidget'
base = importlib.import_module(base)
widget = getattr(base, wc)
return widget(parent)

# }}}

class StoreBase(Plugin): # {{{ supported_platforms = ['windows', 'osx', 'linux']
author = 'John Schember'
type = _('Store')
# Information about the store. Should be in the primary language
# of the store. This should not be translatable when set by
# a subclass.
description = _('An e-book store.')
minimum_calibre_version = (0, 8, 0)
version = (1, 0, 1)

actual_plugin = None

# Does the store only distribute e-books without DRM.
drm_free_only = False
# This is the 2 letter country code for the corporate
# headquarters of the store.
headquarters = ''
# All formats the store distributes e-books in.
formats = []
# Is this store on an affiliate program?
affiliate = False def load_actual_plugin(self, gui):
'''
This method must return the actual interface action plugin object.
'''
mod, cls = self.actual_plugin.split(':')
self.actual_plugin_object = getattr(importlib.import_module(mod), cls)(gui, self.name)
return self.actual_plugin_object def customization_help(self, gui=False):
if getattr(self, 'actual_plugin_object', None) is not None:
return self.actual_plugin_object.customization_help(gui)
raise NotImplementedError()

def config_widget(self):
if getattr(self, 'actual_plugin_object', None) is not None:
return self.actual_plugin_object.config_widget()
raise NotImplementedError()

def save_settings(self, config_widget):
if getattr(self, 'actual_plugin_object', None) is not None:
return self.actual_plugin_object.save_settings(config_widget)
raise NotImplementedError()

# }}}

class EditBookToolPlugin(Plugin): # {{{ type = _('Edit book tool')
minimum_calibre_version = (1, 46, 0)

# }}}

class LibraryClosedPlugin(Plugin): # {{{
'''
LibraryClosedPlugins are run when a library is closed, either at shutdown, when the library is changed, or when a library is used in some other way.
At the moment these plugins won't be called by the CLI functions.
'''
type = _('Library closed')

# minimum version 2.54 because that is when support was added minimum_calibre_version = (2, 54, 0)

def run(self, db):
'''
The db will be a reference to the new_api (db.cache.py).

The plugin must run to completion. It must not use the GUI, threads, or any signals.
'''
raise NotImplementedError('LibraryClosedPlugin '
'run method must be overridden in subclass')
# }}}

class AIProviderPlugin(Plugin): # {{{
'''
AIProvider plugins abstract AI services that can be used by the rest of calibre.
'''
type = _('AI provider')

# minimum version when support for this plugin type was added minimum_calibre_version = (8, 10, 0)

# Used by builtin AI Provider plugins to live load the backend code builtin_live_module_name = ''

# See the AICapabilities enum. Sub-classes *must* implement this to the
# capabilities they support. Note this is independent of configuration.
@property def capabilities(self):
raise NotImplementedError()

@property def builtin_live_module(self):
if not self.builtin_live_module_name:
return None if (ans := self._builtin_live_module) is None:
from calibre.live import Strategy, load_module ans = self._builtin_live_module = load_module(self.builtin_live_module_name, strategy=Strategy.fast)
return ans

@property def is_ready_for_use(self) -> bool:
if not self.builtin_live_module_name:
return False return self.builtin_live_module.is_ready_for_use()

def initialize(self):
self._builtin_live_module = None def customization_help(self):
return ''

def config_widget(self):
'''
The config widget for an AI plugin must support validate() and additionally the property is_ready_for_use which must be true iff the plugin is ready to be used, i.e. it does not require configuration such as an API key or the API key is already set.
'''
if self.builtin_live_module_name:
return self.builtin_live_module.config_widget()
raise NotImplementedError()

def save_settings(self, config_widget):
if self.builtin_live_module_name:
return self.builtin_live_module.save_settings(config_widget)
raise NotImplementedError()

def text_chat(self, messages: Iterable[ChatMessage], use_model: str = '') -> Iterator[ChatResponse]:
'''
Send the specified chat messages to the AI and return an iterable over its streaming responses.
The :code:`use_model` parameter will cause the plugin to use a specific model, useful when having a conversation, to ensure the same model responds for every subsequent query.
'''
if not self.builtin_live_module_name:
raise NotImplementedError()
yield from self.builtin_live_module.text_chat(messages, use_model)

def human_readable_model_name(self, model_id: str) -> str:
' Return a human readable model name for the specified model id'
if not self.builtin_live_module_name:
return model_id return self.builtin_live_module.human_readable_model_name(model_id)
# }}}

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 시작 »
- 모듈 코드 »
- calibre.customize »
- calibre.customize.conversion

calibre.customize.conversion의 소스 코드

'''
Defines the plugin system for conversions.
'''
import numbers import os import re import shutil from calibre import CurrentDir from calibre.customize import Plugin class ConversionOption:
'''
Class representing conversion options
'''

def __init__(self, name=None, help=None, long_switch=None, short_switch=None, choices=None):
self.name = name self.help = help self.long_switch = long_switch self.short_switch = short_switch self.choices = choices if self.long_switch is None:
self.long_switch = self.name.replace('_', '-')

self.validate_parameters()

def validate_parameters(self):
'''
Validate the parameters passed to :meth:`__init__`.
'''
if re.match(r'[a-zA-Z_]([a-zA-Z0-9_])*', self.name) is None:
raise ValueError(self.name + ' is not a valid Python identifier')
if not self.help:
raise ValueError('You must set the help text')

def __hash__(self):
return hash(self.name)

def __eq__(self, other):
return self.name == getattr(other, 'name', other)

def clone(self):
return ConversionOption(name=self.name, help=self.help, long_switch=self.long_switch, short_switch=self.short_switch, choices=self.choices)

class OptionRecommendation:
LOW = 1
MED = 2
HIGH = 3 def __init__(self, recommended_value=None, level=LOW, **kwargs):
'''
An option recommendation. That is, an option as well as its recommended value and the level of the recommendation.
'''
self.level = level self.recommended_value = recommended_value self.option = kwargs.pop('option', None)
if self.option is None:
self.option = ConversionOption(**kwargs)

self.validate_parameters()

@property def help(self):
return self.option.help def clone(self):
return OptionRecommendation(recommended_value=self.recommended_value, level=self.level, option=self.option.clone())

def validate_parameters(self):
if self.option.choices and self.recommended_value not in \ self.option.choices:
raise ValueError(f'OpRec: {self.option.name}: Recommended value not in choices')
if not (isinstance(self.recommended_value, (numbers.Number, bytes, str)) or self.recommended_value is None):
raise ValueError(f'OpRec: {self.option.name}:' + repr( self.recommended_value) + ' is not a string or a number')

class DummyReporter:

def __init__(self):
self.cancel_requested = False def __call__(self, percent, msg=''):
pass def gui_configuration_widget(name, parent, get_option_by_name, get_option_help, db, book_id, for_output=True):
import importlib def widget_factory(cls):
return cls(parent, get_option_by_name, get_option_help, db, book_id)

if for_output:
try:
output_widget = importlib.import_module(
'calibre.gui2.convert.'+name)
pw = output_widget.PluginWidget pw.ICON = 'back.png'
pw.HELP = _('Options specific to the output format.')
return widget_factory(pw)
except ImportError:
pass else:
try:
input_widget = importlib.import_module(
'calibre.gui2.convert.'+name)
pw = input_widget.PluginWidget pw.ICON = 'forward.png'
pw.HELP = _('Options specific to the input format.')
return widget_factory(pw)
except ImportError:
pass return None

[문서]
class InputFormatPlugin(Plugin):
'''
InputFormatPlugins are responsible for converting a document into
HTML+OPF+CSS+etc.
The results of the conversion *must* be encoded in UTF-8.
The main action happens in :meth:`convert`.
'''

type = _('Conversion input')
supported_platforms = ['windows', 'osx', 'linux']
commit_name = None # unique name under which options for this plugin are saved ui_data = None

#: Set of file types for which this plugin should be run
#: For example: ``set(['azw', 'mobi', 'prc'])`` file_types = set()

#: If True, this input plugin generates a collection of images,
#: one per HTML file. This can be set dynamically, in the convert method
#: if the input files can be both image collections and non-image collections.
#: If you set this to True, you must implement the get_images() method that returns
#: a list of images.
is_image_collection = False

#: Number of CPU cores used by this plugin.
#: A value of -1 means that it uses all available cores core_usage = 1

#: If set to True, the input plugin will perform special processing
#: to make its output suitable for viewing for_viewer = False

#: The encoding that this input plugin creates files in. A value of
#: None means that the encoding is undefined and must be
#: detected individually output_encoding = 'utf-8'

#: Options shared by all Input format plugins. Do not override
#: in sub-classes. Use :attr:`options` instead. Every option must be an
#: instance of :class:`OptionRecommendation`.
common_options = {
OptionRecommendation(name='input_encoding', recommended_value=None, level=OptionRecommendation.LOW, help=_('Specify the character encoding of the input document. If '
'set this option will override any encoding declared by the '
'document itself. Particularly useful for documents that '
'do not declare an encoding or that have erroneous '
'encoding declarations.')
)}

#: Options to customize the behavior of this plugin. Every option must be an
#: instance of :class:`OptionRecommendation`.
options = set()

#: A set of 3-tuples of the form
#: (option_name, recommended_value, recommendation_level)
recommendations = set()

def __init__(self, *args):
Plugin.__init__(self, *args)
self.report_progress = DummyReporter()

[문서]
def get_images(self):
'''
Return a list of absolute paths to the images, if this input plugin represents an image collection. The list of images is in the same order as the spine and the TOC.
'''
raise NotImplementedError()

[문서]
def convert(self, stream, options, file_ext, log, accelerators):
'''
This method must be implemented in sub-classes. It must return the path to the created OPF file or an :class:`OEBBook` instance.
All output should be contained in the current folder.
If this plugin creates files outside the current folder they must be deleted/marked for deletion before this method returns.

:param stream: A file like object that contains the input file.
:param options: Options to customize the conversion process.
Guaranteed to have attributes corresponding to all the options declared by this plugin. In addition, it will have a verbose attribute that takes integral values from zero upwards. Higher numbers mean be more verbose. Another useful attribute is
``input_profile`` that is an instance of
:class:`calibre.customize.profiles.InputProfile`.
:param file_ext: The extension (without the .) of the input file. It is guaranteed to be one of the `file_types` supported by this plugin.
:param log: A :class:`calibre.utils.logging.Log` object. All output should use this object.
:param accelerators: A dictionary of various information that the input plugin can get easily that would speed up the subsequent stages of the conversion.

'''
raise NotImplementedError()

def __call__(self, stream, options, file_ext, log, accelerators, output_dir):
try:
log(f'InputFormatPlugin: {self.name} running')
if hasattr(stream, 'name'):
log('on', stream.name)
except Exception:
# In case stdout is broken pass with CurrentDir(output_dir):
for x in os.listdir('.'):
shutil.rmtree(x) if os.path.isdir(x) else os.remove(x)

ret = self.convert(stream, options, file_ext, log, accelerators)

return ret

[문서]
def postprocess_book(self, oeb, opts, log):
'''
Called to allow the input plugin to perform postprocessing after the book has been parsed.
'''
pass

[문서]
def specialize(self, oeb, opts, log, output_fmt):
'''
Called to allow the input plugin to specialize the parsed book for a particular output format. Called after postprocess_book and before any transforms are performed on the parsed book.
'''
pass

[문서]
def gui_configuration_widget(self, parent, get_option_by_name, get_option_help, db, book_id=None):
'''
Called to create the widget used for configuring this plugin in the calibre GUI. The widget must be an instance of the PluginWidget class.
See the builtin input plugins for examples.
'''
name = self.name.lower().replace(' ', '_')
return gui_configuration_widget(name, parent, get_option_by_name, get_option_help, db, book_id, for_output=False)

[문서]
class OutputFormatPlugin(Plugin):
'''
OutputFormatPlugins are responsible for converting an OEB document
(OPF+HTML) into an output e-book.

The OEB document can be assumed to be encoded in UTF-8.
The main action happens in :meth:`convert`.
'''

type = _('Conversion output')
supported_platforms = ['windows', 'osx', 'linux']
commit_name = None # unique name under which options for this plugin are saved ui_data = None

#: The file type (extension without leading period) that this
#: plugin outputs file_type = None

#: Options shared by all Input format plugins. Do not override
#: in sub-classes. Use :attr:`options` instead. Every option must be an
#: instance of :class:`OptionRecommendation`.
common_options = {
OptionRecommendation(name='pretty_print', recommended_value=False, level=OptionRecommendation.LOW, help=_('If specified, the output plugin will try to create output '
'that is as human readable as possible. May not have any effect '
'for some output plugins.')
)}

#: Options to customize the behavior of this plugin. Every option must be an
#: instance of :class:`OptionRecommendation`.
options = set()

#: A set of 3-tuples of the form
#: (option_name, recommended_value, recommendation_level)
recommendations = set()

@property def description(self):
return _('Convert e-books to the %s format')%self.file_type.upper()

def __init__(self, *args):
Plugin.__init__(self, *args)
self.report_progress = DummyReporter()

[문서]
def convert(self, oeb_book, output, input_plugin, opts, log):
'''
Render the contents of `oeb_book` (which is an instance of
:class:`calibre.ebooks.oeb.OEBBook`) to the file specified by output.

:param output: Either a file like object or a string. If it is a string it is the path to a folder that may or may not exist. The output plugin should write its output into that folder. If it is a file like object, the output plugin should write its output into the file.
:param input_plugin: The input plugin that was used at the beginning of the conversion pipeline.
:param opts: Conversion options. Guaranteed to have attributes corresponding to the OptionRecommendations of this plugin.
:param log: The logger. Print debug/info messages etc. using this.

'''
raise NotImplementedError()

@property def is_periodical(self):
return self.oeb.metadata.publication_type and \ str(self.oeb.metadata.publication_type[0]).startswith('periodical:')

[문서]
def specialize_options(self, log, opts, input_fmt):
'''
Can be used to change the values of conversion options, as used by the conversion pipeline.
'''
pass

[문서]
def specialize_css_for_output(self, log, opts, item, stylizer):
'''
Can be used to make changes to the CSS during the CSS flattening process.

:param item: The item (HTML file) being processed
:param stylizer: A Stylizer object containing the flattened styles for item. You can get the style for any element by stylizer.style(element).

'''
pass

[문서]
def gui_configuration_widget(self, parent, get_option_by_name, get_option_help, db, book_id=None):
'''
Called to create the widget used for configuring this plugin in the calibre GUI. The widget must be an instance of the PluginWidget class.
See the builtin output plugins for examples.
'''
name = self.name.lower().replace(' ', '_')
return gui_configuration_widget(name, parent, get_option_by_name, get_option_help, db, book_id, for_output=True)

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 시작 »
- 모듈 코드 »
- calibre.db.cache

calibre.db.cache의 소스 코드

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import hashlib import operator import os import random import shutil import sys import traceback import weakref from collections import defaultdict from collections.abc import Iterable, Iterator, MutableSet, Set from contextlib import contextmanager from datetime import datetime from functools import partial, wraps from io import DEFAULT_BUFFER_SIZE, BytesIO from queue import Queue, ShutDown from threading import Lock from time import mktime, monotonic, time from typing import NamedTuple from calibre import as_unicode, detect_ncpus, isbytestring from calibre.constants import iswindows, preferred_encoding from calibre.customize.ui import run_plugins_on_import, run_plugins_on_postadd, run_plugins_on_postdelete, run_plugins_on_postimport from calibre.db import SPOOL_SIZE, _get_next_series_num_for_list from calibre.db.annotations import merge_annotations from calibre.db.categories import get_categories from calibre.db.constants import COVER_FILE_NAME, DATA_DIR_NAME, NOTES_DIR_NAME, Pages from calibre.db.errors import NoSuchBook, NoSuchFormat from calibre.db.fields import IDENTITY, InvalidLinkTable, create_field from calibre.db.lazy import FormatMetadata, FormatsList, ProxyMetadata from calibre.db.listeners import EventDispatcher, EventType from calibre.db.locking import DowngradeLockError, LockingError, SafeReadLock, create_locks, try_lock from calibre.db.notes.connect import copy_marked_up_text from calibre.db.page_count import MaintainPageCounts from calibre.db.search import Search from calibre.db.tables import VirtualTable from calibre.db.utils import type_safe_sort_key_function from calibre.db.write import get_series_values, sqlite_datetime, uniq from calibre.ebooks import check_ebook_format from calibre.ebooks.metadata import author_to_author_sort, string_to_authors, title_sort from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.metadata.opf2 import metadata_to_opf from calibre.ptempfile import PersistentTemporaryFile, SpooledTemporaryFile, base_dir from calibre.utils.config import prefs, tweaks from calibre.utils.date import UNDEFINED_DATE, is_date_undefined, timestampfromdt, utcnow from calibre.utils.date import now as nowf from calibre.utils.filenames import make_long_path_useable from calibre.utils.icu import lower as icu_lower from calibre.utils.icu import sort_key from calibre.utils.iso8601 import parse_iso8601 from calibre.utils.localization import canonicalize_lang from polyglot.builtins import cmp class ExtraFile(NamedTuple):
relpath: str file_path: str stat_result: os.stat_result def api(f):
f.is_cache_api = True return f def read_api(f):
f = api(f)
f.is_read_api = True return f def write_api(f):
f = api(f)
f.is_read_api = False return f def wrap_simple(lock, func):
@wraps(func)
def call_func_with_lock(*args, **kwargs):
try:
with lock:
return func(*args, **kwargs)
except DowngradeLockError:
# We already have an exclusive lock, no need to acquire a shared
# lock. See the safe_read_lock properties' documentation for why
# this is necessary.
return func(*args, **kwargs)
return call_func_with_lock def run_import_plugins(path_or_stream, fmt):
fmt = fmt.lower()
if hasattr(path_or_stream, 'seek'):
path_or_stream.seek(0)
pt = PersistentTemporaryFile('_import_plugin.'+fmt)
shutil.copyfileobj(path_or_stream, pt, 1024**2)
pt.close()
path = pt.name else:
path = path_or_stream return run_plugins_on_import(path, fmt)

def _add_newbook_tag(mi):
tags = prefs['new_book_tags']
if tags:
for tag in [t.strip() for t in tags]:
if tag:
if not mi.tags:
mi.tags = [tag]
elif tag not in mi.tags:
mi.tags.append(tag)

def _add_default_custom_column_values(mi, fm):
cols = fm.custom_field_metadata(include_composites=False)
for cc,col in cols.items():
dv = col['display'].get('default_value', None)
try:
if dv is not None:
if not mi.get_user_metadata(cc, make_copy=False):
mi.set_user_metadata(cc, col)
dt = col['datatype']
if dt == 'datetime' and icu_lower(dv) == 'now':
dv = nowf()
mi.set(cc, dv)
except Exception:
traceback.print_exc()

dynamic_category_preferences = frozenset({'grouped_search_make_user_categories', 'grouped_search_terms', 'user_categories'})

[문서]
class Cache:
'''
An in-memory cache of the metadata.db file from a calibre library.
This class also serves as a threadsafe API for accessing the database.
The in-memory cache is maintained in normal form for maximum performance.

SQLITE is simply used as a way to read and write from metadata.db robustly.
All table reading/sorting/searching/caching logic is re-implemented. This was necessary for maximum performance and flexibility.
'''
EventType = EventType fts_indexing_sleep_time = 4 # seconds def __init__(self, backend, library_database_instance=None):
self.shutting_down = False self.is_doing_rebuild_or_vacuum = False self.backend = backend self.maintain_page_counts = MaintainPageCounts(self)
self.maintain_page_counts.start()
# We want templates to have access to LibraryDatabase if we have it,
# otherwise this instance (Cache)
self.database_instance = (weakref.ref(self) if library_database_instance is None else weakref.ref(library_database_instance))
self.event_dispatcher = EventDispatcher()
self.fields = {}
self.composites = {}
self.read_lock, self.write_lock = create_locks()
self.format_metadata_cache = defaultdict(dict)
self.formatter_template_cache = {}
self.dirtied_cache = {}
self.link_maps_cache = {}
self.extra_files_cache = {}
self.vls_for_books_cache = None self.vls_for_books_lib_in_process = None self.vls_cache_lock = Lock()
self.dirtied_sequence = 0 self.cover_caches = set()
self.clear_search_cache_count = 0

# Implement locking for all simple read/write API methods
# An unlocked version of the method is stored with the name starting
# with a leading underscore. Use the unlocked versions when the lock
# has already been acquired.
for name in dir(self):
func = getattr(self, name)
ira = getattr(func, 'is_read_api', None)
if ira is not None:
# Save original function setattr(self, '_'+name, func)
# Wrap it in a lock lock = self.read_lock if ira else self.write_lock setattr(self, name, wrap_simple(lock, func))

self._search_api = Search(self, 'saved_searches', self.field_metadata.get_search_terms())
self.initialize_dynamic()
self.initialize_fts()

@property def new_api(self):
return self

@property def library_id(self):
return self.backend.library_id

@property def dbpath(self):
return self.backend.dbpath

@property def is_fat_filesystem(self):
return self.backend.is_fat_filesystem

@property def safe_read_lock(self):
''' A safe read lock is a lock that does nothing if the thread already has a write lock, otherwise it acquires a read lock. This is necessary to prevent DowngradeLockErrors, which can happen when updating the search cache in the presence of composite columns. Updating the search cache holds an exclusive lock, but searching a composite column involves reading field values via ProxyMetadata which tries to get a shared lock. There may be other scenarios that trigger this as well.

This property returns a new lock object on every access. This lock object is not recursive (for performance) and must only be used in a with statement as ``with cache.safe_read_lock:`` otherwise bad things will happen.'''
return SafeReadLock(self.read_lock)

@write_api def ensure_has_search_category(self, fail_on_existing=True):
if len(self._search_api.saved_searches.names()) > 0:
self.field_metadata.add_search_category(label='search', name=_('Saved searches'), fail_on_existing=fail_on_existing)

def _initialize_dynamic_categories(self):
# Reconstruct the user categories, putting them into field_metadata fm = self.field_metadata fm.remove_dynamic_categories()
for user_cat in sorted(self._pref('user_categories', {}), key=sort_key):
cat_name = '@' + user_cat # add the '@' to avoid name collision while cat_name:
try:
fm.add_user_category(label=cat_name, name=user_cat)
except ValueError:
break # Can happen since we are removing dots and adding parent categories ourselves cat_name = cat_name.rpartition('.')[0]

# add grouped search term user categories muc = frozenset(self._pref('grouped_search_make_user_categories', []))
for cat in sorted(self._pref('grouped_search_terms', {}), key=sort_key):
if cat in muc:
# There is a chance that these can be duplicates of an existing
# user category. Print the exception and continue.
try:
self.field_metadata.add_user_category(label='@' + cat, name=cat)
except ValueError:
traceback.print_exc()
self._ensure_has_search_category()

self.field_metadata.add_grouped_search_terms( self._pref('grouped_search_terms', {}))
self._refresh_search_locations()

@write_api def initialize_dynamic(self):
self.backend.dirty_books_with_dirtied_annotations()
self.dirtied_cache = {x:i for i, x in enumerate(self.backend.dirtied_books())}
if self.dirtied_cache:
self.dirtied_sequence = max(self.dirtied_cache.values())+1 self._initialize_dynamic_categories()

@write_api def initialize_template_cache(self):
self.formatter_template_cache = {}

@write_api def set_user_template_functions(self, user_template_functions):
self.backend.set_user_template_functions(user_template_functions)

@write_api def clear_composite_caches(self, book_ids=None):
for field in self.composites.values():
field.clear_caches(book_ids=book_ids)

@write_api def clear_search_caches(self, book_ids=None):
self.clear_search_cache_count += 1 self._search_api.update_or_clear(self, book_ids)
self.vls_for_books_cache = None self.vls_for_books_lib_in_process = None

@write_api def clear_extra_files_cache(self, book_id=None):
if book_id is None:
self.extra_files_cache = {}
else:
self.extra_files_cache.pop(book_id, None)

@read_api def last_modified(self):
return self.backend.last_modified()

def __enter__(self):
self.backend.__enter__()

def __exit__(self, exc_type, exc_value, tb):
self.backend.__exit__(exc_type, exc_value, tb)

@write_api def clear_caches(self, book_ids=None, template_cache=True, search_cache=True):
if template_cache:
self._initialize_template_cache() # Clear the formatter template cache for field in self.fields.values():
if hasattr(field, 'clear_caches'):
field.clear_caches(book_ids=book_ids) # Clear the composite cache and ondevice caches if book_ids:
for book_id in book_ids:
self.format_metadata_cache.pop(book_id, None)
else:
self.format_metadata_cache.clear()
if search_cache:
self._clear_search_caches(book_ids)
self._clear_link_map_cache(book_ids)

@write_api def clear_link_map_cache(self, book_ids=None):
if book_ids is None:
self.link_maps_cache = {}
else:
for book in book_ids:
self.link_maps_cache.pop(book, None)

@write_api def reload_from_db(self, clear_caches=True):
if clear_caches:
self._clear_caches()
with self.backend.conn: # Prevent other processes, such as calibredb from interrupting the reload by locking the db self.backend.prefs.load_from_db()
self._search_api.saved_searches.load_from_db()
for field in self.fields.values():
if hasattr(field, 'table'):
field.table.read(self.backend) # Reread data from metadata.db

@property def field_metadata(self):
return self.backend.field_metadata def _get_metadata(self, book_id, get_user_categories=True): # {{{ mi = Metadata(None, template_cache=self.formatter_template_cache)

mi._proxy_metadata = ProxyMetadata(self, book_id, formatter=mi.formatter)

author_ids = self._field_ids_for('authors', book_id)
adata = self._author_data(author_ids)
aut_list = [adata[i] for i in author_ids]
aum = []
aus = {}
for rec in aut_list:
aut = rec['name']
aum.append(aut)
aus[aut] = rec['sort']
mi.title = self._field_for('title', book_id, default_value=_('Unknown'))
mi.authors = aum mi.author_sort = self._field_for('author_sort', book_id, default_value=_('Unknown'))
mi.author_sort_map = aus mi.comments = self._field_for('comments', book_id)
mi.publisher = self._field_for('publisher', book_id)
n = utcnow()
mi.timestamp = self._field_for('timestamp', book_id, default_value=n)
mi.pubdate = self._field_for('pubdate', book_id, default_value=n)
mi.uuid = self._field_for('uuid', book_id, default_value='dummy')
mi.title_sort = self._field_for('sort', book_id, default_value=_('Unknown'))
mi.last_modified = self._field_for('last_modified', book_id, default_value=n)
formats = self._field_for('formats', book_id)
mi.format_metadata = {}
mi.languages = list(self._field_for('languages', book_id))
mi.pages = self._field_for('pages', book_id, default_value=0)
if not formats:
good_formats = None else:
mi.format_metadata = FormatMetadata(self, book_id, formats)
good_formats = FormatsList(sorted(formats), mi.format_metadata)
# These three attributes are returned by the db2 get_metadata(),
# however, we don't actually use them anywhere other than templates, so
# they have been removed, to avoid unnecessary overhead. The templates
# all use _proxy_metadata.
# mi.book_size = self._field_for('size', book_id, default_value=0)
# mi.ondevice_col = self._field_for('ondevice', book_id, default_value='')
# mi.db_approx_formats = formats mi.formats = good_formats mi.has_cover = _('Yes') if self._field_for('cover', book_id, default_value=False) else ''
mi.tags = list(self._field_for('tags', book_id, default_value=()))
mi.series = self._field_for('series', book_id)
if mi.series:
mi.series_index = self._field_for('series_index', book_id, default_value=1.0)
mi.rating = self._field_for('rating', book_id)
mi.set_identifiers(self._field_for('identifiers', book_id, default_value={}))
mi.application_id = book_id mi.id = book_id for key, meta in self.field_metadata.custom_iteritems():
mi.set_user_metadata(key, meta)
if meta['datatype'] != 'composite':
# composites are evaluated on demand in metadata.book.base
# because their value is None val = self._field_for(key, book_id)
if isinstance(val, tuple):
val = list(val)
extra = self._field_for(key+'_index', book_id)
mi.set(key, val=val, extra=extra)

mi.link_maps = self._get_all_link_maps_for_book(book_id)

user_cat_vals = {}
if get_user_categories:
user_cats = self._pref('user_categories', {})
for ucat in user_cats:
res = []
for name,cat,ign in user_cats[ucat]:
v = mi.get(cat, None)
if isinstance(v, list):
if name in v:
res.append([name,cat])
elif name == v:
res.append([name,cat])
user_cat_vals[ucat] = res mi.user_categories = user_cat_vals return mi
# }}}

[문서]
@api def init(self):
'''
Initialize this cache with data from the backend.
'''
with self.write_lock:
self.backend.read_tables()
bools_are_tristate = self.backend.prefs['bools_are_tristate']

for field, table in self.backend.tables.items():
self.fields[field] = create_field(field, table, bools_are_tristate, self.backend.get_template_functions, self.database_instance)
if table.metadata['datatype'] == 'composite':
self.composites[field] = self.fields[field]

self.fields['ondevice'] = create_field('ondevice', VirtualTable('ondevice'), bools_are_tristate, self.backend.get_template_functions, self.database_instance)

for name, field in self.fields.items():
if name[0] == '#' and name.endswith('_index'):
field.series_field = self.fields[name[:-len('_index')]]
self.fields[name[:-len('_index')]].index_field = field elif name == 'series_index':
field.series_field = self.fields['series']
self.fields['series'].index_field = field elif name == 'authors':
field.author_sort_field = self.fields['author_sort']
elif name == 'title':
field.title_sort_field = self.fields['sort']
if self.backend.prefs.get('full_page_scan_requested'):
self._queue_pages_scan(by_user=False)
if self.backend.prefs['update_all_last_mod_dates_on_start']:
self.update_last_modified(self.all_book_ids())
self.backend.prefs.set('update_all_last_mod_dates_on_start', False)# FTS API {{{ def initialize_fts(self):
self.fts_queue_thread = None self.fts_measuring_rate = None self.fts_num_done_since_start = 0 self.fts_job_queue = Queue()
self.fts_indexing_left = self.fts_indexing_total = 0 fts = self.backend.initialize_fts(weakref.ref(self))
if self.is_fts_enabled():
self.start_fts_pool()
return fts def start_fts_pool(self):
from threading import Event, Thread self.fts_dispatch_stop_event = Event()
self.fts_queue_thread = Thread(name='FTSQueue', target=Cache.dispatch_fts_jobs, args=( self.fts_job_queue, self.fts_dispatch_stop_event, weakref.ref(self)), daemon=True)
self.fts_queue_thread.start()
self.backend.fts.pool.initialize()
self.backend.fts.pool.initialized.wait()
self.queue_next_fts_job()

@read_api def is_fts_enabled(self):
return self.backend.fts_enabled

@write_api def fts_start_measuring_rate(self, measure=True):
self.fts_measuring_rate = monotonic() if measure else None self.fts_num_done_since_start = 0 def _update_fts_indexing_numbers(self, job_time=None):
# this is called when new formats are added and when a format is
# indexed, but NOT when books or formats are deleted, so total may not
# be up to date.
nl = self.backend.fts.number_dirtied()
nt = self.backend.get('SELECT COUNT(*) FROM main.data')[0][0] or 0 if not nl:
self._fts_start_measuring_rate(measure=False)
if job_time is not None and self.fts_measuring_rate is not None:
self.fts_num_done_since_start += 1 if (self.fts_indexing_left, self.fts_indexing_total) != (nl, nt) or job_time is not None:
self.fts_indexing_left = nl self.fts_indexing_total = nt self.event_dispatcher(EventType.indexing_progress_changed, *self._fts_indexing_progress())

@read_api def fts_indexing_progress(self):
rate = None if self.fts_measuring_rate is not None and self.fts_num_done_since_start > 4:
rate = self.fts_num_done_since_start / (monotonic() - self.fts_measuring_rate)
return self.fts_indexing_left, self.fts_indexing_total, rate

@write_api def enable_fts(self, enabled=True, start_pool=True):
fts = self.backend.enable_fts(weakref.ref(self) if enabled else None)
if fts and start_pool: # used in the tests self.start_fts_pool()
if not fts and self.fts_queue_thread:
self.fts_job_queue.put(None)
self.fts_queue_thread = None self.fts_job_queue = Queue()
if fts:
self._update_fts_indexing_numbers()
return fts

@write_api def fts_unindex(self, book_id, fmt=None):
self.backend.fts_unindex(book_id, fmt=fmt)

@staticmethod def dispatch_fts_jobs(queue, stop_dispatch, dbref):
from .fts.text import is_fmt_extractable def do_one():
self = dbref()
if self is None:
return False start_time = monotonic()
with self.read_lock:
if not self.backend.fts_enabled:
return False book_id, fmt = self.backend.get_next_fts_job()
if book_id is None:
return False path = self._format_abspath(book_id, fmt)
if not path or not is_fmt_extractable(fmt):
with self.write_lock:
self.backend.remove_dirty_fts(book_id, fmt)
self._update_fts_indexing_numbers()
return True with self.read_lock, open(path, 'rb') as src, PersistentTemporaryFile(suffix=f'.{fmt.lower()}') as pt:
sz = 0 h = hashlib.sha1()
while True:
chunk = src.read(DEFAULT_BUFFER_SIZE)
if not chunk:
break sz += len(chunk)
h.update(chunk)
pt.write(chunk)
with self.write_lock:
queued = self.backend.queue_fts_job(book_id, fmt, pt.name, sz, h.hexdigest(), start_time)
if not queued: # means a dirtied book was removed from the dirty list because the text has not changed self._update_fts_indexing_numbers(monotonic() - start_time)
return self.backend.fts_has_idle_workers def loop_while_more_available():
self = dbref()
if not self or not self.backend.fts_enabled:
return has_more = True while has_more and not self.shutting_down and self.backend.fts_enabled and not stop_dispatch.is_set():
try:
has_more = do_one()
except Exception:
if self.backend.fts_enabled:
traceback.print_exc()
if stop_dispatch.wait(self.fts_indexing_sleep_time):
break while True:
try:
x = queue.get()
except ShutDown:
break if x is None:
break loop_while_more_available()

@write_api def queue_next_fts_job(self):
if not self.backend.fts_enabled:
return self.fts_job_queue.put(True)
self._update_fts_indexing_numbers()

@write_api def commit_fts_result(self, book_id, fmt, fmt_size, fmt_hash, text, err_msg, start_time):
ans = self.backend.commit_fts_result(book_id, fmt, fmt_size, fmt_hash, text, err_msg)
self._update_fts_indexing_numbers(monotonic() - start_time)
return ans

@write_api def reindex_fts_book(self, book_id, *fmts):
if not self.is_fts_enabled():
return if not fmts:
fmts = self._formats(book_id)
self.backend.reindex_fts_book(book_id, *fmts)
self._queue_next_fts_job()

@api def reindex_fts(self):
if not self.is_fts_enabled():
return with self.write_lock:
self._shutdown_fts()
self._shutdown_fts(stage=2)
with self.write_lock:
self.backend.reindex_fts()
fts = self.initialize_fts()
fts.initialize(self.backend.conn) # ensure fts is pre-initialized needed for the tests self._queue_next_fts_job()
return fts

@write_api def set_fts_num_of_workers(self, num):
existing = self.backend.fts_num_of_workers if num != existing:
self.backend.fts_num_of_workers = num if num > existing:
self._queue_next_fts_job()
return True return False

@write_api def set_fts_speed(self, slow=True):
orig = self.fts_indexing_sleep_time if slow:
self.fts_indexing_sleep_time = Cache.fts_indexing_sleep_time changed = self._set_fts_num_of_workers(1)
else:
self.fts_indexing_sleep_time = 0.1 changed = self._set_fts_num_of_workers(max(1, detect_ncpus()))
changed = changed or orig != self.fts_indexing_sleep_time if changed and self.fts_measuring_rate is not None:
self._fts_start_measuring_rate()
return changed

@write_api # we need to use write locking as SQLITE gives a locked table error if multiple FTS queries are made at the same time def fts_search( self, fts_engine_query, use_stemming=True, highlight_start=None, highlight_end=None, snippet_size=None, restrict_to_book_ids=None, return_text=True, result_type=tuple, process_each_result=None,
):
return result_type(self.backend.fts_search( fts_engine_query, use_stemming=use_stemming, highlight_start=highlight_start, highlight_end=highlight_end, snippet_size=snippet_size, return_text=return_text, restrict_to_book_ids=restrict_to_book_ids, process_each_result=process_each_result,
))

# }}}

# Notes API {{{

[문서]
@read_api def notes_for(self, field, item_id) -> str:
' Return the notes document or an empty string if not found '
return self.backend.notes_for(field, item_id)

[문서]
@read_api def notes_data_for(self, field, item_id) -> str:
' Return all notes data as a dict or None if note does not exist '
return self.backend.notes_data_for(field, item_id)

[문서]
@read_api def get_all_items_that_have_notes(self, field_name=None) -> set[int] | dict[str, set[int]]:
' Return all item_ids for items that have notes in the specified field or all fields if field_name is None '
return self.backend.get_all_items_that_have_notes(field_name)

[문서]
@read_api def field_supports_notes(self, field=None) -> bool:
' Return True iff the specified field supports notes. If field is None return frozenset of all fields that support notes. '
if field is None:
return self.backend.notes.allowed_fields return field in self.backend.notes.allowed_fields

[문서]
@read_api def items_with_notes_in_book(self, book_id: int) -> dict[str, dict[int, str]]:
' Return a dict of field to items that have associated notes for that field for the specified book '
ans = {}
for k in self.backend.notes.allowed_fields:
try:
field = self.fields[k]
except KeyError:
continue v = {}
for item_id in field.ids_for_book(book_id):
if self.backend.notes_for(k, item_id):
v[item_id] = field.table.id_map[item_id]
if v:
ans[k] = v return ans

[문서]
@write_api def set_notes_for(self, field, item_id, doc: str, searchable_text: str = copy_marked_up_text, resource_hashes=(), remove_unused_resources=False) -> int:
'''
Set the notes document. If the searchable text is different from the document, specify it as searchable_text. If the document references resources their hashes must be present in resource_hashes. Set remove_unused_resources to True to cleanup unused resources, note that updating a note automatically cleans up resources pertaining to that note anyway.
'''
ans = self.backend.set_notes_for(field, item_id, doc, searchable_text, resource_hashes, remove_unused_resources)
self.event_dispatcher(EventType.notes_changed, field, frozenset({item_id}))
return ans

[문서]
@write_api def add_notes_resource(self, path_or_stream_or_data, name: str, mtime: float = None) -> int:
' Add the specified resource so it can be referenced by notes and return its content hash '
return self.backend.add_notes_resource(path_or_stream_or_data, name, mtime)

[문서]
@read_api def get_notes_resource(self, resource_hash) -> dict | None:
' Return a dict containing the resource data and name or None if no resource with the specified hash is found '
return self.backend.get_notes_resource(resource_hash)

[문서]
@read_api def notes_resources_used_by(self, field, item_id):
' Return the set of resource hashes of all resources used by the note for the specified item '
return frozenset(self.backend.notes_resources_used_by(field, item_id))

[문서]
@write_api def unretire_note_for(self, field, item_id) -> int:
' Unretire a previously retired note for the specified item. Notes are retired when an item is removed from the database '
ans = self.backend.unretire_note_for(field, item_id)
self.event_dispatcher(EventType.notes_changed, field, frozenset({item_id}))
return ans

[문서]
@read_api def export_note(self, field, item_id) -> str:
' Export the note as a single HTML document with embedded images as data: URLs '
return self.backend.export_note(field, item_id)

[문서]
@write_api def import_note(self, field, item_id, path_to_html_file, path_is_data=False):
' Import a previously exported note or an arbitrary HTML file as the note for the specified item '
if path_is_data:
html = path_to_html_file ctime = mtime = time()
basedir = base_dir()
else:
with open(path_to_html_file, 'rb') as f:
html = f.read()
st = os.stat(f.fileno())
ctime, mtime = st.st_ctime, st.st_mtime basedir = os.path.dirname(os.path.abspath(path_to_html_file))
ans = self.backend.import_note(field, item_id, html, basedir, ctime, mtime)
self.event_dispatcher(EventType.notes_changed, field, frozenset({item_id}))
return ans

[문서]
@write_api # we need to use write locking as SQLITE gives a locked table error if multiple FTS queries are made at the same time def search_notes( self, fts_engine_query='', use_stemming=True, highlight_start=None, highlight_end=None, snippet_size=None, restrict_to_fields=(), return_text=True, result_type=tuple, process_each_result=None, limit=None,
):
' Search the text of notes using an FTS index. If the query is empty return all notes. '
return result_type(self.backend.search_notes( fts_engine_query, use_stemming=use_stemming, highlight_start=highlight_start, highlight_end=highlight_end, snippet_size=snippet_size, return_text=return_text, restrict_to_fields=restrict_to_fields, process_each_result=process_each_result, limit=limit,
))

# }}}

# Cache Layer API {{{

[문서]
@write_api def add_listener(self, event_callback_function, check_already_added=False):
'''
Register a callback function that will be called after certain actions are taken on this database. The function must take three arguments:
(:class:`EventType`, library_id, event_type_specific_data)
'''
self.event_dispatcher.library_id = getattr(self, 'server_library_id', self.library_id)
if check_already_added and event_callback_function in self.event_dispatcher:
return False self.event_dispatcher.add_listener(event_callback_function)
return True

@write_api def remove_listener(self, event_callback_function):
self.event_dispatcher.remove_listener(event_callback_function)

[문서]
@read_api def field_for(self, name, book_id, default_value=None):
'''
Return the value of the field ``name`` for the book identified by ``book_id``. If no such book exists or it has no defined value for the field ``name`` or no such field exists, then
``default_value`` is returned.

``default_value`` is not used for title, title_sort, authors, author_sort and series_index. This is because these always have values in the db.
``default_value`` is used for all custom columns.

The returned value for is_multiple fields are always tuples, even when no values are found (in other words, default_value is ignored). The exception is identifiers for which the returned value is always a dictionary.
The returned tuples are always in link order, that is, the order in which they were created.
'''
if self.composites and name in self.composites:
return self.composite_for(name, book_id, default_value=default_value)
try:
field = self.fields[name]
except KeyError:
return default_value if field.is_multiple:
default_value = field.default_value try:
return field.for_book(book_id, default_value=default_value)
except (KeyError, IndexError):
return default_value

[문서]
@read_api def fast_field_for(self, field_obj, book_id, default_value=None):
' Same as field_for, except that it avoids the extra lookup to get the field object '
if field_obj.is_composite:
return field_obj.get_value_with_cache(book_id, self._get_proxy_metadata)
if field_obj.is_multiple:
default_value = field_obj.default_value try:
return field_obj.for_book(book_id, default_value=default_value)
except (KeyError, IndexError):
return default_value

[문서]
@read_api def all_field_for(self, field, book_ids, default_value=None):
' Same as field_for, except that it operates on multiple books at once '
field_obj = self.fields[field]
return {book_id:self._fast_field_for(field_obj, book_id, default_value=default_value) for book_id in book_ids}

@read_api def composite_for(self, name, book_id, mi=None, default_value=''):
try:
f = self.fields[name]
except KeyError:
return default_value if mi is None:
return f.get_value_with_cache(book_id, self._get_proxy_metadata)
else:
return f._render_composite_with_cache(book_id, mi, mi.formatter, mi.template_cache)

[문서]
@read_api def field_ids_for(self, name, book_id):
'''
Return the ids (as a tuple) for the values that the field ``name`` has on the book identified by ``book_id``. If there are no values, or no such book, or no such field, an empty tuple is returned.
'''
try:
return self.fields[name].ids_for_book(book_id)
except (KeyError, IndexError):
return ()

[문서]
@read_api def books_for_field(self, name, item_id):
'''
Return all the books associated with the item identified by
``item_id``, where the item belongs to the field ``name``.

Returned value is a set of book ids, or the empty set if the item or the field does not exist.
'''
try:
return self.fields[name].books_for(item_id)
except (KeyError, IndexError):
return set()

@read_api def books_by_year(self, field: str = 'pubdate', restrict_to_books: Iterable[int] | None = None) -> dict[int, set[int]]:
books_by_year = defaultdict(set)
m = self.field_metadata[field]
value_column = m.get('column') or field table = m.get('table') or 'books'
book_col = 'id' if table == 'books' else 'book'
query = f'SELECT CAST(substr({value_column}, 1, 4) AS INTEGER), {book_col} FROM {table}'
if restrict_to_books is None:
for year, book_id in self.backend.execute(query):
books_by_year[year].add(book_id)
else:
books = tuple(restrict_to_books)
BATCH_SIZE = self.backend.max_number_of_variables for i in range(0, len(books), BATCH_SIZE):
batch = books[i:i + BATCH_SIZE]
placeholders = '?,' * len(batch)
for year, book_id in self.backend.execute(query + f' WHERE {book_col} IN ({placeholders[:-1]})', batch):
books_by_year[year].add(book_id)
return dict(books_by_year)

@read_api def books_by_month(self, field: str = 'pubdate', restrict_to_books: Iterable[int] | None = None) -> dict[tuple[int, int], set[int]]:
m = self.field_metadata[field]
value_column = m.get('column') or field table = m.get('table') or 'books'
book_col = 'id' if table == 'books' else 'book'

query = f'''
SELECT
CAST(substr({value_column}, 1, 4) AS INTEGER),
CAST(substr({value_column}, 6, 2) AS INTEGER),
{book_col}
FROM {table}
'''
ans = defaultdict(set)
if restrict_to_books is None:
for year, month, book_id in self.backend.execute(query):
ans[(year, month)].add(book_id)
else:
books = tuple(restrict_to_books)
BATCH_SIZE = self.backend.max_number_of_variables for i in range(0, len(books), BATCH_SIZE):
batch = books[i:i + BATCH_SIZE]
placeholders = '?,' * len(batch)
for year, month, book_id in self.backend.execute( query + f' WHERE {book_col} IN ({placeholders[:-1]})', batch):
ans[(year, month)].add(book_id)
return dict(ans)

[문서]
@read_api def all_book_ids(self, type=frozenset):
'''
Frozen set of all known book ids.
'''
return type(self.fields['uuid'].table.book_col_map)

[문서]
@read_api def all_field_ids(self, name):
'''
Frozen set of ids for all values in the field ``name``.
'''
return frozenset(iter(self.fields[name]))

[문서]
@read_api def all_field_names(self, field):
''' Frozen set of all fields names (should only be used for many-one and many-many fields) '''
if field == 'formats':
return frozenset(self.fields[field].table.col_book_map)

try:
return frozenset(self.fields[field].table.id_map.values())
except AttributeError:
raise ValueError(f'{field} is not a many-one or many-many field')

[문서]
@read_api def get_usage_count_by_id(self, field):
''' Return a mapping of id to usage count for all values of the specified field, which must be a many-one or many-many field. '''
try:
return {k:len(v) for k, v in self.fields[field].table.col_book_map.items()}
except AttributeError:
raise ValueError(f'{field} is not a many-one or many-many field')

[문서]
@read_api def get_id_map(self, field):
''' Return a mapping of id numbers to values for the specified field.
The field must be a many-one or many-many field, otherwise a ValueError is raised. '''
try:
return self.fields[field].table.id_map.copy()
except AttributeError:
if field == 'title':
return self.fields[field].table.book_col_map.copy()
raise ValueError(f'{field} is not a many-one or many-many field')

[문서]
@read_api def get_item_name(self, field, item_id):
''' Return the item name for the item specified by item_id in the specified field. See also :meth:`get_id_map`.'''
return self.fields[field].table.id_map[item_id]

[문서]
@read_api def get_item_id(self, field, item_name, case_sensitive=False):
''' Return the item id for item_name or None if not found.
This function is very slow if doing lookups for multiple names use either get_item_ids() or get_item_name_map().
Similarly, case sensitive lookups are faster than case insensitive ones. '''
field = self.fields[field]
if hasattr(field, 'item_ids_for_names'):
d = field.item_ids_for_names(self.backend, (item_name,), case_sensitive)
for v in d.values():
return v

[문서]
@read_api def get_item_ids(self, field, item_names, case_sensitive=False):
' Return a dict mapping item_name to the item id or None '
field = self.fields[field]
if hasattr(field, 'item_ids_for_names'):
return field.item_ids_for_names(self.backend, item_names, case_sensitive)
return dict.fromkeys(item_names)

[문서]
@read_api def get_item_name_map(self, field, normalize_func=None):
' Return mapping of item values to ids '
if normalize_func is None:
return {v:k for k, v in self.fields[field].table.id_map.items()}
return {normalize_func(v):k for k, v in self.fields[field].table.id_map.items()}

[문서]
@read_api def get_book_path(self, book_id, sep=os.sep, unsafe=False):
'''
Return the relative book path for the given id.
Prefer this because you can choose the directory separator, default use the os one.
If unsafe is True, allow to return None if the book_id is not in the library.
'''
rslt = self._field_for('path', book_id)
if rslt:
if sep != '/':
rslt = rslt.replace('/', sep)
return rslt if unsafe:
return None raise KeyError(f'No book with id {book_id!r} found in the library')

[문서]
@read_api def author_data(self, author_ids=None) -> dict[int, dict[str, str]]:
'''
Return author data as a dictionary with keys: name, sort, link

If no authors with the specified ids are found an empty dictionary is returned. If author_ids is None, data for all authors is returned.
'''
af = self.fields['authors']
if author_ids is None:
return {aid:af.author_data(aid) for aid in af.table.id_map}
return {aid:af.author_data(aid) for aid in author_ids if aid in af.table.id_map}

[문서]
@read_api def author_sorts(self, author_ids=None) -> dict[int, str]:
'''
Return author sorts for specified authors.

If no authors with the specified ids are found an empty dictionary is returned. If author_ids is None, data for all authors is returned.
'''
af = self.fields['authors']
m = af.table.asort_map if author_ids is None:
return m.copy()
return {aid:m[aid] for aid in author_ids if aid in m}

[문서]
@read_api def format_hash(self, book_id, fmt):
''' Return the hash of the specified format for the specified book. The kind of hash is backend dependent, but is usually SHA-256. '''
try:
name = self.fields['formats'].format_fname(book_id, fmt)
path = self._get_book_path(book_id)
except Exception:
raise NoSuchFormat(f'Record {book_id} has no fmt: {fmt}')
return self.backend.format_hash(book_id, fmt, name, path)

[문서]
@api def format_metadata(self, book_id, fmt, allow_cache=True, update_db=False):
'''
Return the path, size and mtime for the specified format for the specified book.
You should not use path unless you absolutely have to, since accessing it directly breaks the threadsafe guarantees of this API. Instead use the :meth:`copy_format_to` method.

:param allow_cache: If ``True`` cached values are used, otherwise a slow filesystem access is done. The cache values could be out of date if access was performed to the filesystem outside of this API.

:param update_db: If ``True`` The max_size field of the database is updated for this book.
'''
if not fmt:
return {}
fmt = fmt.upper()
# allow_cache and update_db are mutually exclusive. Give priority to update_db if allow_cache and not update_db:
x = self.format_metadata_cache[book_id].get(fmt, None)
if x is not None:
return x with self.safe_read_lock:
try:
name = self.fields['formats'].format_fname(book_id, fmt)
path = self._get_book_path(book_id)
except Exception:
return {}

ans = {}
if path and name:
ans = self.backend.format_metadata(book_id, fmt, name, path)
self.format_metadata_cache[book_id][fmt] = ans if update_db and 'size' in ans:
with self.write_lock:
max_size = self.fields['formats'].table.update_fmt(book_id, fmt, name, ans['size'], self.backend)
self.fields['size'].table.update_sizes({book_id: max_size})

return ans@read_api def format_files(self, book_id):
field = self.fields['formats']
fmts = field.table.book_col_map.get(book_id, ())
return {fmt:field.format_fname(book_id, fmt) for fmt in fmts}

@read_api def format_db_size(self, book_id, fmt):
field = self.fields['formats']
return field.format_size(book_id, fmt)

[문서]
@read_api def pref(self, name, default=None, namespace=None, get_default_from_defaults=False):
' Return the value for the specified preference or the value specified as ``default`` if the preference is not set. '
p = self.backend.prefs if get_default_from_defaults:
default = p.defaults.get(name, default)
if namespace is not None:
return p.get_namespaced(namespace, name, default)
return p.get(name, default)

[문서]
@write_api def set_pref(self, name, val, namespace=None):
' Set the specified preference to the specified value. See also :meth:`pref`. '
if namespace is not None:
self.backend.prefs.set_namespaced(namespace, name, val)
return self.backend.prefs.set(name, val)
if name in ('grouped_search_terms', 'virtual_libraries'):
self._clear_search_caches()
if name in dynamic_category_preferences:
self._initialize_dynamic_categories()

[문서]
@api def get_metadata(self, book_id, get_cover=False, get_user_categories=True, cover_as_data=False):
'''
Return metadata for the book identified by book_id as a :class:`calibre.ebooks.metadata.book.base.Metadata` object.
Note that the list of formats is not verified. If get_cover is True, the cover is returned, either a path to temp file as mi.cover or if cover_as_data is True then as mi.cover_data.
'''

# Check if virtual_libraries_for_books rebuilt its cache. If it did then
# we must clear the composite caches so the new data can be taken into
# account. Clearing the caches requires getting a write lock, so it must
# be done outside of the closure of _get_metadata().
composite_cache_needs_to_be_cleared = False with self.safe_read_lock:
vl_cache_was_none = self.vls_for_books_cache is None mi = self._get_metadata(book_id, get_user_categories=get_user_categories)
if vl_cache_was_none and self.vls_for_books_cache is not None:
composite_cache_needs_to_be_cleared = True if composite_cache_needs_to_be_cleared:
try:
self.clear_composite_caches()
except LockingError:
# We can't clear the composite caches because a read lock is set.
# As a consequence the value of a composite column that calls
# virtual_libraries() might be wrong. Oh well. Log and keep running.
print("Couldn't get write lock after vls_for_books_cache was loaded", file=sys.stderr)
traceback.print_exc()

if get_cover:
if cover_as_data:
cdata = self.cover(book_id)
if cdata:
mi.cover_data = ('jpeg', cdata)
else:
mi.cover = self.cover(book_id, as_path=True)

return mi

[문서]
@read_api def get_proxy_metadata(self, book_id):
''' Like :meth:`get_metadata` except that it returns a ProxyMetadata object that only reads values from the database on demand. This is much faster than get_metadata when only a small number of fields need to be accessed from the returned metadata object. '''
return ProxyMetadata(self, book_id)

[문서]
@api def cover(self, book_id, as_file=False, as_image=False, as_path=False, as_pixmap=False):
'''
Return the cover image or None. By default, returns the cover as a bytestring.

WARNING: Using as_path will copy the cover to a temp file and return the path to the temp file. You should delete the temp file when you are done with it.

:param as_file: If True return the image as an open file object (a SpooledTemporaryFile)
:param as_image: If True return the image as a QImage object
:param as_pixmap: If True return the image as a QPixmap object
:param as_path: If True return the image as a path pointing to a temporary file
'''
if as_file:
ret = SpooledTemporaryFile(SPOOL_SIZE)
if not self.copy_cover_to(book_id, ret):
ret.close()
return ret.seek(0)
elif as_path:
pt = PersistentTemporaryFile('_dbcover.jpg')
with pt:
if not self.copy_cover_to(book_id, pt):
return ret = pt.name elif as_pixmap or as_image:
from qt.core import QImage, QPixmap ret = QImage() if as_image else QPixmap()
with self.safe_read_lock:
path = self._format_abspath(book_id, '__COVER_INTERNAL__')
if path:
ret.load(path)
else:
buf = BytesIO()
if not self.copy_cover_to(book_id, buf):
return ret = buf.getvalue()
return ret

@read_api def cover_or_cache(self, book_id, timestamp, as_what='bytes'):
try:
path = self._get_book_path(book_id)
except (AttributeError, KeyError):
return False, None, None return self.backend.cover_or_cache(path, timestamp, as_what)

@read_api def cover_last_modified(self, book_id: int) -> datetime | None:
try:
path = self._get_book_path(book_id)
except (AttributeError, KeyError):
return return self.backend.cover_last_modified(path)

@read_api def cover_timestamp(self, book_id: int) -> float | None:
try:
path = self._get_book_path(book_id)
except (AttributeError, KeyError):
return return self.backend.cover_timestamp(path)

[문서]
@read_api def copy_cover_to(self, book_id, dest, use_hardlink=False, report_file_size=None):
'''
Copy the cover to the file like object ``dest``. Returns False if no cover exists or dest is the same file as the current cover.
dest can also be a path in which case the cover is copied to it if and only if the path is different from the current path (taking case sensitivity into account).
'''
try:
path = self._get_book_path(book_id)
except (AttributeError, KeyError):
return False return self.backend.copy_cover_to(path, dest, use_hardlink=use_hardlink, report_file_size=report_file_size)

[문서]
@write_api def compress_covers(self, book_ids, jpeg_quality=100, progress_callback=None):
'''
Compress the cover images for the specified books. A compression quality of 100 will perform lossless compression, otherwise lossy compression.

The progress callback will be called with the book_id and the old and new sizes for each book that has been processed. If an error occurs, the new size will be a string with the error details.
'''
jpeg_quality = max(10, min(jpeg_quality, 100))
path_map = {}
for book_id in book_ids:
try:
path_map[book_id] = self._get_book_path(book_id)
except (AttributeError, KeyError):
continue self.backend.compress_covers(path_map, jpeg_quality, progress_callback)

[문서]
@read_api def copy_format_to(self, book_id, fmt, dest, use_hardlink=False, report_file_size=None):
'''
Copy the format ``fmt`` to the file like object ``dest``. If the specified format does not exist, raises :class:`NoSuchFormat` error.
dest can also be a path (to a file), in which case the format is copied to it, iff the path is different from the current path (taking case sensitivity into account).
'''
fmt = (fmt or '').upper()
try:
name = self.fields['formats'].format_fname(book_id, fmt)
path = self._get_book_path(book_id)
except (KeyError, AttributeError):
raise NoSuchFormat(f'Record {book_id} has no {fmt} file')

return self.backend.copy_format_to(book_id, fmt, name, path, dest, use_hardlink=use_hardlink, report_file_size=report_file_size)

[문서]
@read_api def format_abspath(self, book_id, fmt):
'''
Return absolute path to the e-book file of format `format`. You should almost never use this, as it breaks the threadsafe promise of this API.
Instead use, :meth:`copy_format_to`.

Currently used only in calibredb list, the viewer, edit book, compare_format to original format, open with, bulk metadata edit and the catalogs (via get_data_as_dict()).

Apart from the viewer, open with and edit book, I don't believe any of the others do any file write I/O with the results of this call.
'''
fmt = (fmt or '').upper()
try:
path = self._get_book_path(book_id)
except Exception:
return None if path:
if fmt == '__COVER_INTERNAL__':
return self.backend.cover_abspath(book_id, path)
else:
try:
name = self.fields['formats'].format_fname(book_id, fmt)
except Exception:
return None if name:
return self.backend.format_abspath(book_id, fmt, name, path)

[문서]
@read_api def has_format(self, book_id, fmt):
'Return True iff the format exists on disk'
fmt = (fmt or '').upper()
try:
name = self.fields['formats'].format_fname(book_id, fmt)
path = self._get_book_path(book_id)
except Exception:
return False return self.backend.has_format(book_id, fmt, name, path)

[문서]
@api def save_original_format(self, book_id, fmt):
' Save a copy of the specified format as ORIGINAL_FORMAT, overwriting any existing ORIGINAL_FORMAT. '
fmt = fmt.upper()
if 'ORIGINAL' in fmt:
raise ValueError('Cannot save original of an original fmt')
fmtfile = self.format(book_id, fmt, as_file=True)
if fmtfile is None:
return False with fmtfile:
nfmt = 'ORIGINAL_'+fmt return self.add_format(book_id, nfmt, fmtfile, run_hooks=False)

[문서]
@write_api def restore_original_format(self, book_id, original_fmt):
''' Restore the specified format from the previously saved
ORIGINAL_FORMAT, if any. Return True on success. The ORIGINAL_FORMAT is deleted after a successful restore. '''
original_fmt = original_fmt.upper()
fmt = original_fmt.partition('_')[2]
try:
ofmt_name = self.fields['formats'].format_fname(book_id, original_fmt)
path = self._get_book_path(book_id)
except Exception:
return False if self.backend.is_format_accessible(book_id, original_fmt, ofmt_name, path):
self.add_format(book_id, fmt, BytesIO(), run_hooks=False)
fmt_name = self.fields['formats'].format_fname(book_id, fmt)
file_size = self.backend.rename_format_file(book_id, ofmt_name, original_fmt, fmt_name, fmt, path)
self.fields['formats'].table.update_fmt(book_id, fmt, fmt_name, file_size, self.backend)
self._remove_formats({book_id:(original_fmt,)})
return True return False

[문서]
@read_api def formats(self, book_id, verify_formats=True):
'''
Return tuple of all formats for the specified book. If verify_formats is True, verifies that the files exist on disk.
'''
ans = self.field_for('formats', book_id)
if verify_formats and ans:
try:
path = self._get_book_path(book_id)
except Exception:
return ()

def verify(fmt):
try:
name = self.fields['formats'].format_fname(book_id, fmt)
except Exception:
return False return self.backend.has_format(book_id, fmt, name, path)

ans = tuple(x for x in ans if verify(x))
return ans

[문서]
@api def format(self, book_id, fmt, as_file=False, as_path=False, preserve_filename=False):
'''
Return the e-book format as a bytestring or `None` if the format doesn't exist, or we don't have permission to write to the e-book file.

:param as_file: If True the e-book format is returned as a file object. Note that the file object is a SpooledTemporaryFile, so if what you want to do is copy the format to another file, use :meth:`copy_format_to` instead for performance.
:param as_path: Copies the format file to a temp file and returns the path to the temp file
:param preserve_filename: If True and returning a path the filename is the same as that used in the library. Note that using this means that repeated calls yield the same temp file (which is re-created each time)
'''
fmt = (fmt or '').upper()
ext = ('.'+fmt.lower()) if fmt else ''
if as_path:
if preserve_filename:
with self.safe_read_lock:
try:
fname = self.fields['formats'].format_fname(book_id, fmt)
except Exception:
return None fname += ext bd = base_dir()
d = os.path.join(bd, 'format_abspath')
try:
os.makedirs(d)
except Exception:
pass ret = os.path.join(d, fname)
try:
self.copy_format_to(book_id, fmt, ret)
except NoSuchFormat:
return None else:
with PersistentTemporaryFile(ext) as pt:
try:
self.copy_format_to(book_id, fmt, pt)
except NoSuchFormat:
return None ret = pt.name elif as_file:
with self.safe_read_lock:
try:
fname = self.fields['formats'].format_fname(book_id, fmt)
except Exception:
return None fname += ext ret = SpooledTemporaryFile(SPOOL_SIZE)
try:
self.copy_format_to(book_id, fmt, ret)
except NoSuchFormat:
ret.close()
return None ret.seek(0)
# Various bits of code try to use the name as the default
# title when reading metadata, so set it ret.name = fname else:
buf = BytesIO()
try:
self.copy_format_to(book_id, fmt, buf)
except NoSuchFormat:
return None ret = buf.getvalue()

return ret@read_api def newly_added_book_ids(self, count=5, book_ids=None) -> list[int]:
ids_to_sort = self._all_book_ids(list) if book_ids is None else list(book_ids)
ids_to_sort.sort(reverse=True)
return ids_to_sort[:count]

@read_api def size_stats(self) -> dict[str, int]:
return self.backend.size_stats()

[문서]
@read_api def multisort(self, fields, ids_to_sort=None, virtual_fields=None):
'''
Return a list of sorted book ids. If ids_to_sort is None, all book ids are returned.

fields must be a list of 2-tuples of the form (field_name, ascending=True or False). The most significant field is the first
2-tuple.
'''
ids_to_sort = self._all_book_ids() if ids_to_sort is None else ids_to_sort get_metadata = self._get_proxy_metadata lang_map = self.fields['languages'].book_value_map virtual_fields = virtual_fields or {}

fm = {'title':'sort', 'authors':'author_sort'}

def sort_key_func(field):
'Handle series type fields, virtual fields and the id field'
idx = field + '_index'
is_series = idx in self.fields try:
func = self.fields[fm.get(field, field)].sort_keys_for_books(get_metadata, lang_map)
except KeyError:
if field == 'id':
return IDENTITY else:
return virtual_fields[fm.get(field, field)].sort_keys_for_books(get_metadata, lang_map)
if is_series:
idx_func = self.fields[idx].sort_keys_for_books(get_metadata, lang_map)

def skf(book_id):
return (func(book_id), idx_func(book_id))
return skf return func

# Sort only once on any given field fields = uniq(fields, operator.itemgetter(0))

if len(fields) == 1:
keyfunc = sort_key_func(fields[0][0])
reverse = not fields[0][1]
try:
return sorted(ids_to_sort, key=keyfunc, reverse=reverse)
except Exception as err:
print('Failed to sort database on field:', fields[0][0], 'with error:', err, file=sys.stderr)
try:
return sorted(ids_to_sort, key=type_safe_sort_key_function(keyfunc), reverse=reverse)
except Exception as err:
print('Failed to type-safe sort database on field:', fields[0][0], 'with error:', err, file=sys.stderr)
return sorted(ids_to_sort, reverse=reverse)
sort_key_funcs = tuple(sort_key_func(field) for field, order in fields)
orders = tuple(1 if order else -1 for _, order in fields)
Lazy = object() # Lazy load the sort keys for sub-sort fields class SortKey:

__slots__ = 'book_id', 'sort_key'

def __init__(self, book_id):
self.book_id = book_id
# Calculate only the first sub-sort key since that will always be used self.sort_key = [key(book_id) if i == 0 else Lazy for i, key in enumerate(sort_key_funcs)]

def compare_to_other(self, other):
for i, (order, self_key, other_key) in enumerate(zip(orders, self.sort_key, other.sort_key)):
if self_key is Lazy:
self_key = self.sort_key[i] = sort_key_funcs[i](self.book_id)
if other_key is Lazy:
other_key = other.sort_key[i] = sort_key_funcs[i](other.book_id)
ans = cmp(self_key, other_key)
if ans != 0:
return ans * order return 0 def __eq__(self, other):
return self.compare_to_other(other) == 0 def __ne__(self, other):
return self.compare_to_other(other) != 0 def __lt__(self, other):
return self.compare_to_other(other) < 0 def __le__(self, other):
return self.compare_to_other(other) <= 0 def __gt__(self, other):
return self.compare_to_other(other) > 0 def __ge__(self, other):
return self.compare_to_other(other) >= 0 return sorted(ids_to_sort, key=SortKey)

[문서]
@read_api def search(self, query, restriction='', virtual_fields=None, book_ids=None, allow_templates=True):
'''
Search the database for the specified query, returning a set of matched book ids.

:param restriction: A restriction that is ANDed to the specified query. Note that restrictions are cached, therefore the search for a AND b will be slower than a with restriction b.

:param virtual_fields: Used internally (virtual fields such as on_device to search over).

:param book_ids: If not None, a set of book ids for which books will be searched instead of searching all books.
'''
return self._search_api(self, query, restriction, virtual_fields=virtual_fields, book_ids=book_ids, allow_templates=allow_templates)

[문서]
@read_api def books_in_virtual_library(self, vl, search_restriction=None, virtual_fields=None):
' Return the set of books in the specified virtual library '
vl = self._pref('virtual_libraries', {}).get(vl) if vl else None if not vl and not search_restriction:
return self.all_book_ids()
# We utilize the search restriction cache to speed this up srch = partial(self._search, virtual_fields=virtual_fields)
if vl:
if search_restriction:
return frozenset(srch('', vl) & srch('', search_restriction))
return frozenset(srch('', vl))
return frozenset(srch('', search_restriction))

@read_api def number_of_books_in_virtual_library(self, vl=None, search_restriction=None):
if not vl and not search_restriction:
return len(self.fields['uuid'].table.book_col_map)
return len(self.books_in_virtual_library(vl, search_restriction))

[문서]
@api def get_categories(self, sort='name', book_ids=None, already_fixed=None, first_letter_sort=False, uncollapsed_categories=None):
' Used internally to implement the Tag Browser '
try:
with self.safe_read_lock:
return get_categories(self, sort=sort, book_ids=book_ids, first_letter_sort=first_letter_sort, uncollapsed_categories=uncollapsed_categories)
except InvalidLinkTable as err:
bad_field = err.field_name if bad_field == already_fixed:
raise with self.write_lock:
self.fields[bad_field].table.fix_link_table(self.backend)
return self.get_categories(sort=sort, book_ids=book_ids, already_fixed=bad_field)

@write_api def update_last_modified(self, book_ids, now=None):
if book_ids:
if now is None:
now = nowf()
f = self.fields['last_modified']
f.writer.set_books({book_id:now for book_id in book_ids}, self.backend)
if self.composites:
self._clear_composite_caches(book_ids)
self._clear_search_caches(book_ids)

@write_api def mark_as_dirty(self, book_ids):
self._update_last_modified(book_ids)
already_dirtied = set(self.dirtied_cache).intersection(book_ids)
new_dirtied = book_ids - already_dirtied already_dirtied = {book_id:self.dirtied_sequence+i for i, book_id in enumerate(already_dirtied)}
if already_dirtied:
self.dirtied_sequence = max(already_dirtied.values()) + 1 self.dirtied_cache.update(already_dirtied)
if new_dirtied:
self.backend.dirty_books(new_dirtied)
new_dirtied = {book_id:self.dirtied_sequence+i for i, book_id in enumerate(new_dirtied)}
self.dirtied_sequence = max(new_dirtied.values()) + 1 self.dirtied_cache.update(new_dirtied)

@write_api def commit_dirty_cache(self):
if self.dirtied_cache:
self.backend.dirty_books(self.dirtied_cache)

@write_api def check_dirtied_annotations(self):
if not self.backend.dirty_books_with_dirtied_annotations():
return book_ids = set(self.backend.dirtied_books())
new_dirtied = book_ids - set(self.dirtied_cache)
if new_dirtied:
new_dirtied = {book_id:self.dirtied_sequence+i for i, book_id in enumerate(new_dirtied)}
self.dirtied_sequence = max(new_dirtied.values()) + 1 self.dirtied_cache.update(new_dirtied)

[문서]
@write_api def set_field(self, name, book_id_to_val_map, allow_case_change=True, do_path_update=True):
'''
Set the values of the field specified by ``name``. Returns the set of all book ids that were affected by the change.

:param book_id_to_val_map: Mapping of book_ids to values that should be applied.
:param allow_case_change: If True, the case of many-one or many-many fields will be changed.
For example, if a book has the tag ``tag1`` and you set the tag for another book to ``Tag1`` then the both books will have the tag ``Tag1`` if allow_case_change is True, otherwise they will both have the tag ``tag1``.
:param do_path_update: Used internally, you should never change it.
'''
f = self.fields[name]
is_series = f.metadata['datatype'] == 'series'
update_path = name in {'title', 'authors'}
if update_path and iswindows:
paths = (x for x in (self._get_book_path(book_id, sep='/', unsafe=True) for book_id in book_id_to_val_map) if x)
self.backend.windows_check_if_files_in_use(paths)

if is_series:
bimap, simap = {}, {}
sfield = self.fields[name + '_index']
for k, v in book_id_to_val_map.items():
if isinstance(v, (str, bytes)):
v, sid = get_series_values(v)
else:
v = sid = None if sid is None and name.startswith('#'):
sid = self._fast_field_for(sfield, k)
sid = 1.0 if sid is None else sid # The value to be set the db link table bimap[k] = v if sid is not None:
simap[k] = sid book_id_to_val_map = bimap dirtied = f.writer.set_books( book_id_to_val_map, self.backend, allow_case_change=allow_case_change)

if is_series and simap:
sf = self.fields[f.name+'_index']
dirtied |= sf.writer.set_books(simap, self.backend, allow_case_change=False)

if dirtied:
if update_path and do_path_update:
self._update_path(dirtied, mark_as_dirtied=False)
self._mark_as_dirty(dirtied)
self._clear_link_map_cache(dirtied)
self.event_dispatcher(EventType.metadata_changed, name, dirtied)
return dirtied# Page counts {{{

[문서]
@read_api def get_pages(self, book_id: int) -> Pages | None:
' Return page count information for the specified book '
for pages, algorithm, format, format_size, timestamp in self.backend.execute( f'SELECT pages,algorithm,format,format_size,timestamp FROM books_pages_link WHERE book={book_id:d} LIMIT 1'
):
return Pages(int(pages), int(algorithm), str(format), int(format_size), parse_iso8601(timestamp, assume_utc=True))

[문서]
@read_api def pages_needs_scan(self, books: Iterable[int] = ()) -> set[int]:
' Return the subset of books (or all books if empty) that are marked as needing a scan to update page count '
books = tuple(books)
if not books:
return {r[0] for r in self.backend.execute('SELECT book FROM books_pages_link WHERE needs_scan=1')}
ans = set()
BATCH_SIZE = self.backend.max_number_of_variables for i in range(0, len(books), BATCH_SIZE):
batch = books[i:i + BATCH_SIZE]
placeholders = '?,' * len(batch)
for (book_id,) in self.backend.execute( f'SELECT book FROM books_pages_link WHERE needs_scan=1 AND book IN ({placeholders[:-1]})', batch
):
ans.add(book_id)
return ans

@read_api def num_of_books_that_need_pages_counted(self) -> int:
for (ans,) in self.backend.execute('SELECT COUNT(*) FROM books_pages_link WHERE needs_scan=1'):
return ans return 0

[문서]
@write_api def mark_for_pages_recount(self, book_id: int = 0) -> None:
' Mark all books for recount of pages '
if book_id:
self.backend.execute(f'UPDATE books_pages_link SET needs_scan=1 WHERE book={int(book_id)}')
else:
self.backend.execute('UPDATE books_pages_link SET needs_scan=1')

[문서]
@write_api def queue_pages_scan(self, book_id: int = 0, force: bool = False, by_user: bool = True) -> None:
'''
Start a scan updating page counts for all books that need a scan.
If book_id is specified, then only that book is scanned and it is always scanned.
When `force` is True, the existing pages value, if any, is discarded so that the book is forcibly rescanned even if the existing value was up-to-date.
'''
book_id = int(book_id)
if book_id <= 0:
if force:
self.backend.execute('DELETE FROM books_pages_link')
self.fields['pages'].table.book_col_map.clear()
if len(self.fields['pages'].table.book_col_map) < len(self.fields['uuid'].table.book_col_map):
self.backend.execute('INSERT OR IGNORE INTO books_pages_link(book,needs_scan) SELECT id,1 FROM books')
if by_user:
self._set_pref('full_page_scan_requested', True)
elif force:
self.backend.execute(f'DELETE FROM books_pages_link WHERE book={book_id}')
self.fields['pages'].table.book_col_map.pop(book_id, None)
self.backend.execute(f'INSERT INTO books_pages_link(book,needs_scan) VALUES ({book_id},1)')
else:
self.backend.execute(f'UPDATE books_pages_link SET needs_scan=1 WHERE book={book_id}')
self.maintain_page_counts.queue_scan(book_id)@property def page_count_failures_log_path(self) -> str:
return self.maintain_page_counts.failure_log_path

[문서]
@write_api def set_pages( self, book_id: int, pages: int = 0, algorithm: int = 0, format: str = '', format_size: int = 0,
) -> None:
' Set page count information for the specified book '
now = sqlite_datetime(utcnow())
self.backend.execute('''
INSERT INTO books_pages_link (book, pages, algorithm, format, format_size, timestamp, needs_scan) VALUES
(?,?,?,?,?,?,0)
ON CONFLICT(book) DO UPDATE SET pages = excluded.pages, algorithm = excluded.algorithm, format = excluded.format, format_size = excluded.format_size, timestamp = excluded.timestamp, needs_scan = excluded.needs_scan;
''', (book_id, int(pages), int(algorithm), format, int(format_size), now))
self.fields['pages'].table.book_col_map[book_id] = pages self._clear_composite_caches((book_id,))

# }}}

@write_api def update_path(self, book_ids, mark_as_dirtied=True):
for book_id in book_ids:
title = self._field_for('title', book_id, default_value=_('Unknown'))
try:
author = self._field_for('authors', book_id, default_value=(_('Unknown'),))[0]
except IndexError:
author = _('Unknown')
self.backend.update_path(book_id, title, author, self.fields['path'], self.fields['formats'])
self.format_metadata_cache.pop(book_id, None)
if mark_as_dirtied:
self._mark_as_dirty(book_ids)
self._clear_link_map_cache(book_ids)

@read_api def get_a_dirtied_book(self):
if self.dirtied_cache:
return random.choice(tuple(self.dirtied_cache))
return None def _metadata_as_object_for_dump(self, book_id):
mi = self._get_metadata(book_id)
# Always set cover to cover.jpg. Even if cover doesn't exist,
# no harm done. This way no need to call dirtied when
# cover is set/removed mi.cover = 'cover.jpg'
mi.all_annotations = self._all_annotations_for_book(book_id)
return mi

@read_api def get_metadata_for_dump(self, book_id):
mi = None
# get the current sequence number for this book to pass back to the
# backup thread. This will avoid double calls in the case where the
# thread has not done the work between the put and the get_metadata sequence = self.dirtied_cache.get(book_id, None)
if sequence is not None:
try:
# While a book is being created, the path is empty. Don't bother to
# try to write the opf, because it will go to the wrong folder.
if self._get_book_path(book_id, unsafe=True):
mi = self._metadata_as_object_for_dump(book_id)
except Exception:
# This almost certainly means that the book has been deleted while
# the backup operation sat in the queue.
traceback.print_exc()
return mi, sequence

@write_api def clear_dirtied(self, book_id, sequence):
# Clear the dirtied indicator for the books. This is used when fetching
# metadata, creating an OPF, and writing a file are separated into steps.
# The last step is clearing the indicator dc_sequence = self.dirtied_cache.get(book_id, None)
if dc_sequence is None or sequence is None or dc_sequence == sequence:
self.backend.mark_book_as_clean(book_id)
self.dirtied_cache.pop(book_id, None)

@write_api def write_backup(self, book_id, raw):
try:
path = self._get_book_path(book_id)
except Exception:
return self.backend.write_backup(path, raw)

@read_api def dirty_queue_length(self):
return len(self.dirtied_cache)

[문서]
@read_api def read_backup(self, book_id):
''' Return the OPF metadata backup for the book as a bytestring or None if no such backup exists. '''
try:
path = self._get_book_path(book_id)
except Exception:
return try:
return self.backend.read_backup(path)
except OSError:
return None

@write_api def dump_metadata(self, book_ids=None, remove_from_dirtied=True, callback=None):
# Write metadata for each record to an individual OPF file. If callback
# is not None, it is called once at the start with the number of book_ids
# being processed. And once for every book_id, with arguments (book_id,
# mi, ok).
if book_ids is None:
book_ids = set(self.dirtied_cache)

if callback is not None:
callback(len(book_ids), True, False)

for book_id in book_ids:
if self._get_book_path(book_id, unsafe=True) is None:
if callback is not None:
callback(book_id, None, False)
continue mi, sequence = self._get_metadata_for_dump(book_id)
if mi is None:
if callback is not None:
callback(book_id, mi, False)
continue try:
raw = metadata_to_opf(mi)
self._write_backup(book_id, raw)
if remove_from_dirtied:
self._clear_dirtied(book_id, sequence)
except Exception:
pass if callback is not None:
callback(book_id, mi, True)

[문서]
@write_api def set_cover(self, book_id_data_map):
''' Set the cover for this book. The data can be either a QImage,
QPixmap, file object or bytestring. It can also be None, in which case any existing cover is removed. '''

for book_id, data in book_id_data_map.items():
try:
path = self._get_book_path(book_id)
except (KeyError, AttributeError):
self._update_path((book_id,))
path = self._get_book_path(book_id)

self.backend.set_cover(book_id, path, data)
for cc in self.cover_caches:
cc.invalidate(book_id_data_map)
return self._set_field('cover', { book_id:(0 if data is None else 1) for book_id, data in book_id_data_map.items()})

@write_api def add_cover_cache(self, cover_cache):
if not callable(cover_cache.invalidate):
raise ValueError('Cover caches must have an invalidate method')
self.cover_caches.add(cover_cache)

@write_api def remove_cover_cache(self, cover_cache):
self.cover_caches.discard(cover_cache)

[문서]
@write_api def set_metadata(self, book_id, mi, ignore_errors=False, force_changes=False, set_title=True, set_authors=True, allow_case_change=False):
'''
Set metadata for the book `id` from the `Metadata` object `mi`

Setting force_changes=True will force set_metadata to update fields even if mi contains empty values. In this case, 'None' is distinguished from
'empty'. If mi.XXX is None, the XXX is not replaced, otherwise it is.
The tags, identifiers, and cover attributes are special cases. Tags and identifiers cannot be set to None so they will always be replaced if force_changes is true. You must ensure that mi contains the values you want the book to have. Covers are always changed if a new cover is provided, but are never deleted. Also note that force_changes has no effect on setting title or authors.
'''
dirtied = set()

try:
# Handle code passing in an OPF object instead of a Metadata object mi = mi.to_book_metadata()
except (AttributeError, TypeError):
pass def set_field(name, val):
dirtied.update(self._set_field(name, {book_id:val}, do_path_update=False, allow_case_change=allow_case_change))

path_changed = False if set_title and mi.title:
path_changed = True set_field('title', mi.title)
authors_changed = False if set_authors:
path_changed = True if not mi.authors:
mi.authors = [_('Unknown')]
authors = []
for a in mi.authors:
authors += string_to_authors(a)
set_field('authors', authors)
authors_changed = True if path_changed:
self._update_path({book_id})

def protected_set_field(name, val):
try:
set_field(name, val)
except Exception:
if ignore_errors:
traceback.print_exc()
else:
raise

# force_changes has no effect on cover manipulation try:
cdata = mi.cover_data[1]
if cdata is None and isinstance(mi.cover, (str, bytes)) and mi.cover and os.access(mi.cover, os.R_OK):
with open(mi.cover, 'rb') as f:
cdata = f.read() or None if cdata is not None:
self._set_cover({book_id: cdata})
except Exception:
if ignore_errors:
traceback.print_exc()
else:
raise try:
with self.backend.conn: # Speed up set_metadata by not operating in autocommit mode for field in ('rating', 'series_index', 'timestamp'):
val = getattr(mi, field)
if val is not None:
protected_set_field(field, val)

val = mi.get('author_sort', None)
if authors_changed and (not val or mi.is_null('author_sort')):
val = self._author_sort_from_authors(mi.authors)
if authors_changed or (force_changes and val is not None) or not mi.is_null('author_sort'):
protected_set_field('author_sort', val)

for field in ('publisher', 'series', 'tags', 'comments',
'languages', 'pubdate'):
val = mi.get(field, None)
if (force_changes and val is not None) or not mi.is_null(field):
protected_set_field(field, val)

val = mi.get('title_sort', None)
if (force_changes and val is not None) or not mi.is_null('title_sort'):

protected_set_field('sort', val)

# identifiers will always be replaced if force_changes is True mi_idents = mi.get_identifiers()
if force_changes:
protected_set_field('identifiers', mi_idents)
elif mi_idents:
identifiers = self._field_for('identifiers', book_id, default_value={})
for key, val in mi_idents.items():
if val and val.strip(): # Don't delete an existing identifier identifiers[icu_lower(key)] = val protected_set_field('identifiers', identifiers)

user_mi = mi.get_all_user_metadata(make_copy=False)
fm = self.field_metadata for key in user_mi:
if (key in fm and user_mi[key]['datatype'] == fm[key]['datatype'] and ( user_mi[key]['datatype'] != 'text' or ( user_mi[key]['is_multiple'] == fm[key]['is_multiple']))):
val = mi.get(key, None)
if force_changes or val is not None:
protected_set_field(key, val)
idx = key + '_index'
if idx in self.fields:
extra = mi.get_extra(key)
if extra is not None or force_changes:
protected_set_field(idx, extra)
except Exception:
# sqlite will rollback the entire transaction, thanks to the with
# statement, so we have to re-read everything form the db to ensure
# the db and Cache are in sync self._reload_from_db()
raise return dirtieddef _do_add_format(self, book_id, fmt, stream, name=None, mtime=None):
path = self._get_book_path(book_id, unsafe=True)
if path is None:
# Theoretically, this should never happen, but apparently it
# does: https://www.mobileread.com/forums/showthread.php?t=233353 self._update_path({book_id}, mark_as_dirtied=False)
path = self._get_book_path(book_id)

title = self._field_for('title', book_id, default_value=_('Unknown'))
try:
author = self._field_for('authors', book_id, default_value=(_('Unknown'),))[0]
except IndexError:
author = _('Unknown')

size, fname = self.backend.add_format(book_id, fmt, stream, title, author, path, name, mtime=mtime)
return size, fname

[문서]
@api def add_format(self, book_id, fmt, stream_or_path, replace=True, run_hooks=True, dbapi=None):
'''
Add a format to the specified book. Return True if the format was added successfully.

:param replace: If True replace existing format, otherwise if the format already exists, return False.
:param run_hooks: If True, file type plugins are run on the format before and after being added.
:param dbapi: Internal use only.
'''
needs_close = False if run_hooks:
# Run import plugins, the write lock is not held to cater for
# broken plugins that might spin the event loop by popping up a
# message in the GUI during the processing.
npath = run_import_plugins(stream_or_path, fmt)
fmt = os.path.splitext(npath)[-1].lower().replace('.', '').upper()
stream_or_path = open(make_long_path_useable(npath), 'rb')
needs_close = True fmt = check_ebook_format(stream_or_path, fmt)

with self.write_lock:
if not self._has_id(book_id):
raise NoSuchBook(book_id)
fmt = (fmt or '').upper()
self.format_metadata_cache[book_id].pop(fmt, None)
try:
name = self.fields['formats'].format_fname(book_id, fmt)
except Exception:
name = None if name and not replace:
if needs_close:
stream_or_path.close()
return False if hasattr(stream_or_path, 'read'):
stream = stream_or_path else:
stream = open(make_long_path_useable(stream_or_path), 'rb')
needs_close = True try:
size, fname = self._do_add_format(book_id, fmt, stream, name)
finally:
if needs_close:
stream.close()
del stream max_size = self.fields['formats'].table.update_fmt(book_id, fmt, fname, size, self.backend)
self.fields['size'].table.update_sizes({book_id: max_size})
self._update_last_modified((book_id,))
self._queue_pages_scan(book_id)
self.event_dispatcher(EventType.format_added, book_id, fmt)

if run_hooks:
# Run post import plugins, the write lock is released so the plugin
# can call api without a locking violation.
run_plugins_on_postimport(dbapi or self, book_id, fmt)
stream_or_path.close()

self.queue_next_fts_job()
return True[문서]
@write_api def remove_formats(self, formats_map, db_only=False):
'''
Remove the specified formats from the specified books.

:param formats_map: A mapping of book_id to a list of formats to be removed from the book.
:param db_only: If True, only remove the record for the format from the db, do not delete the actual format file from the filesystem.
:return: A map of book id to set of formats actually deleted from the filesystem for that book
'''
table = self.fields['formats'].table formats_map = {book_id:frozenset((f or '').upper() for f in fmts) for book_id, fmts in formats_map.items()}
removed_map = {}

for book_id, fmts in formats_map.items():
for fmt in fmts:
self.format_metadata_cache[book_id].pop(fmt, None)

if not db_only:
removes = defaultdict(set)
metadata_map = {}
for book_id, fmts in formats_map.items():
try:
path = self._get_book_path(book_id)
except Exception:
continue for fmt in fmts:
try:
name = self.fields['formats'].format_fname(book_id, fmt)
except Exception:
continue if name and path:
removes[book_id].add((fmt, name, path))
if removes[book_id]:
metadata_map[book_id] = {'title': self._field_for('title', book_id), 'authors': self._field_for('authors', book_id)}
if removes:
removed_map = self.backend.remove_formats(removes, metadata_map)

size_map = table.remove_formats(formats_map, self.backend)
self.fields['size'].table.update_sizes(size_map)

for book_id, fmts in formats_map.items():
for fmt in fmts:
run_plugins_on_postdelete(self, book_id, fmt)

self._update_last_modified(tuple(formats_map))
for book_id in formats_map:
self._queue_pages_scan(book_id)
self.event_dispatcher(EventType.formats_removed, formats_map)
return removed_map

[문서]
@read_api def get_next_series_num_for(self, series, field='series', current_indices=False):
'''
Return the next series index for the specified series, taking into account the various preferences that control next series number generation.

:param field: The series-like field (defaults to the builtin series column)
:param current_indices: If True, returns a mapping of book_id to current series_index value instead.
'''
books = ()
sf = self.fields[field]
if series:
q = icu_lower(series)
for val, book_ids in sf.iter_searchable_values(self._get_proxy_metadata, frozenset(self._all_book_ids())):
if q == icu_lower(val):
books = book_ids break idf = sf.index_field index_map = {book_id:self._fast_field_for(idf, book_id, default_value=1.0) for book_id in books}
if current_indices:
return index_map series_indices = sorted(index_map.values(), key=lambda s: s or 0)
return _get_next_series_num_for_list(tuple(series_indices), unwrap=False)

[문서]
@read_api def author_sort_from_authors(self, authors, key_func=icu_lower):
'''Given a list of authors, return the author_sort string for the authors, preferring the author sort associated with the author over the computed string. '''
table = self.fields['authors'].table result = []
rmap = {key_func(v):k for k, v in table.id_map.items()}
for aut in authors:
aid = rmap.get(key_func(aut), None)
result.append(author_to_author_sort(aut) if aid is None else table.asort_map[aid])
return ' & '.join(_f for _f in result if _f)

[문서]
@read_api def data_for_has_book(self):
''' Return data suitable for use in :meth:`has_book`. This can be used for an implementation of :meth:`has_book` in a worker process without access to the db. '''
try:
return {icu_lower(title) for title in self.fields['title'].table.book_col_map.values()}
except TypeError:
# Some non-unicode titles in the db return {icu_lower(as_unicode(title)) for title in self.fields['title'].table.book_col_map.values()}

[문서]
@read_api def has_book(self, mi):
''' Return True iff the database contains an entry with the same title as the passed in Metadata object. The comparison is case-insensitive.
See also :meth:`data_for_has_book`. '''
title = mi.title if title:
if isbytestring(title):
title = title.decode(preferred_encoding, 'replace')
q = icu_lower(title).strip()
for title in self.fields['title'].table.book_col_map.values():
if q == icu_lower(title):
return True return False

[문서]
@read_api def has_id(self, book_id):
' Return True iff the specified book_id exists in the db '
return book_id in self.fields['title'].table.book_col_map

@write_api def create_book_entry(self, mi, cover=None, add_duplicates=True, force_id=None, apply_import_tags=True, preserve_uuid=False):
if mi.tags:
mi.tags = list(mi.tags)
if apply_import_tags:
_add_newbook_tag(mi)
_add_default_custom_column_values(mi, self.field_metadata)
if not add_duplicates and self._has_book(mi):
return series_index = (self._get_next_series_num_for(mi.series) if mi.series_index is None else mi.series_index)
try:
series_index = float(series_index)
except Exception:
try:
series_index = float(self._get_next_series_num_for(mi.series))
except Exception:
series_index = 1.0 if not mi.authors:
mi.authors = (_('Unknown'),)
aus = mi.author_sort if not mi.is_null('author_sort') else self._author_sort_from_authors(mi.authors)
mi.title = mi.title or _('Unknown')
if mi.is_null('title_sort'):
mi.title_sort = title_sort(mi.title, lang=mi.languages[0] if mi.languages else None)
if isbytestring(aus):
aus = aus.decode(preferred_encoding, 'replace')
if isbytestring(mi.title):
mi.title = mi.title.decode(preferred_encoding, 'replace')
if force_id is None:
self.backend.execute('INSERT INTO books(title, series_index, author_sort) VALUES (?, ?, ?)',
(mi.title, series_index, aus))
else:
self.backend.execute('INSERT INTO books(id, title, series_index, author_sort) VALUES (?, ?, ?, ?)',
(force_id, mi.title, series_index, aus))
book_id = self.backend.last_insert_rowid()
self.event_dispatcher(EventType.book_created, book_id)

mi.timestamp = utcnow() if (mi.timestamp is None or is_date_undefined(mi.timestamp)) else mi.timestamp mi.pubdate = UNDEFINED_DATE if mi.pubdate is None else mi.pubdate if cover is not None:
mi.cover, mi.cover_data = None, (None, cover)
self._set_metadata(book_id, mi, ignore_errors=True)
lm = getattr(mi, 'link_maps', None)
if lm:
for field, link_map in lm.items():
if self._has_link_map(field):
self._set_link_map(field, link_map, only_set_if_no_existing_link=True)
if preserve_uuid and mi.uuid:
self._set_field('uuid', {book_id:mi.uuid})
# Update the caches for fields from the books table self.fields['size'].table.book_col_map[book_id] = 0 row = next(self.backend.execute('SELECT sort, series_index, author_sort, uuid, has_cover FROM books WHERE id=?', (book_id,)))
for field, val in zip(('sort', 'series_index', 'author_sort', 'uuid', 'cover'), row):
if field == 'cover':
val = bool(val)
elif field == 'uuid':
self.fields[field].table.uuid_to_id_map[val] = book_id self.fields[field].table.book_col_map[book_id] = val return book_id

[문서]
@api def add_books(self, books, add_duplicates=True, apply_import_tags=True, preserve_uuid=False, run_hooks=True, dbapi=None):
'''
Add the specified books to the library. Books should be an iterable of
2-tuples, each 2-tuple of the form :code:`(mi, format_map)` where mi is a
Metadata object and format_map is a dictionary of the form :code:`{fmt: path_or_stream}`, for example: :code:`{'EPUB': '/path/to/file.epub'}`.

Returns a pair of lists: :code:`ids, duplicates`. ``ids`` contains the book ids for all newly created books in the database. ``duplicates`` contains the :code:`(mi, format_map)` for all books that already exist in the database as per the simple duplicate detection heuristic used by :meth:`has_book`.
'''
duplicates, ids = [], []
for mi, format_map in books:
book_id = self.create_book_entry(mi, add_duplicates=add_duplicates, apply_import_tags=apply_import_tags, preserve_uuid=preserve_uuid)
if book_id is None:
duplicates.append((mi, format_map))
else:
fmt_map = {}
ids.append(book_id)
for fmt, stream_or_path in format_map.items():
if self.add_format(book_id, fmt, stream_or_path, dbapi=dbapi, run_hooks=run_hooks):
fmt_map[fmt.lower()] = getattr(stream_or_path, 'name', stream_or_path) or '<stream>'
run_plugins_on_postadd(dbapi or self, book_id, fmt_map)
return ids, duplicates

[문서]
@write_api def remove_books(self, book_ids, permanent=False):
''' Remove the books specified by the book_ids from the database and delete their format files. If ``permanent`` is False, then the format files are placed in the per-library trash directory. '''
path_map = {}
for book_id in book_ids:
try:
path = self._get_book_path(book_id)
except Exception:
path = None path_map[book_id] = path if not permanent and path:
# ensure metadata.opf is written and up-to-date so we can restore the book try:
mi = self._metadata_as_object_for_dump(book_id)
raw = metadata_to_opf(mi)
self.backend.write_backup(path, raw)
except Exception:
traceback.print_exc()
self.backend.remove_books(path_map, permanent=permanent)
for field in self.fields.values():
try:
table = field.table except AttributeError:
continue # Some fields like ondevice do not have tables else:
table.remove_books(book_ids, self.backend)
self._search_api.discard_books(book_ids)
self._clear_caches(book_ids=book_ids, template_cache=False, search_cache=False)
for cc in self.cover_caches:
cc.invalidate(book_ids)
self.event_dispatcher(EventType.books_removed, book_ids)

@read_api def author_sort_strings_for_books(self, book_ids):
val_map = {}
for book_id in book_ids:
authors = self._field_ids_for('authors', book_id)
adata = self._author_data(authors)
val_map[book_id] = tuple(adata[aid]['sort'] for aid in authors)
return val_map

[문서]
@write_api def rename_items(self, field, item_id_to_new_name_map, change_index=True, restrict_to_book_ids=None):
'''
Rename items from a many-one or many-many field such as tags or series.

:param change_index: When renaming in a series-like field also change the series_index values.
:param restrict_to_book_ids: An optional set of book ids for which the rename is to be performed, defaults to all books.
'''
f = self.fields[field]
affected_books = set()
try:
sv = f.metadata['is_multiple']['ui_to_list']
except (TypeError, KeyError, AttributeError):
sv = None if restrict_to_book_ids is not None:
# We have a VL. Only change the item name for those books if not isinstance(restrict_to_book_ids, (Set, MutableSet)):
restrict_to_book_ids = frozenset(restrict_to_book_ids)
id_map = {}
default_process_map = {}
for old_id, new_name in item_id_to_new_name_map.items():
new_names = tuple(x.strip() for x in new_name.split(sv)) if sv else (new_name,)
# Get a list of books in the VL with the item books_with_id = f.books_for(old_id)
books_to_process = books_with_id & restrict_to_book_ids if len(books_with_id) == len(books_to_process):
# All the books with the ID are in the VL, so we can use
# the normal processing default_process_map[old_id] = new_name elif books_to_process:
affected_books.update(books_to_process)
newvals = {}
for book_id in books_to_process:
# Get the current values, remove the one being renamed, then add
# the new value(s) back.
vals = self._field_for(field, book_id)
# Check for is_multiple if isinstance(vals, tuple):
# We must preserve order.
vals = list(vals)
# Don't need to worry about case here because we
# are fetching its one-true spelling. But lets be
# careful anyway try:
dex = vals.index(self._get_item_name(field, old_id))
# This can put the name back with a different case vals[dex] = new_names[0]
# now add any other items if they aren't already there if len(new_names) > 1:
set_vals = {icu_lower(x) for x in vals}
for v in new_names[1:]:
lv = icu_lower(v)
if lv not in set_vals:
vals.append(v)
set_vals.add(lv)
newvals[book_id] = vals except Exception:
traceback.print_exc()
else:
newvals[book_id] = new_names[0]
# Allow case changes self._set_field(field, newvals)
id_map[old_id] = self._get_item_id(field, new_names[0])
if default_process_map:
ab, idm = self._rename_items(field, default_process_map, change_index=change_index)
affected_books.update(ab)
id_map.update(idm)
self.event_dispatcher(EventType.items_renamed, field, affected_books, id_map)
return affected_books, id_map try:
func = f.table.rename_item except AttributeError:
raise ValueError(f'Cannot rename items for one-one fields: {field}')
moved_books = set()
id_map = {}
for item_id, new_name in item_id_to_new_name_map.items():
new_names = tuple(x.strip() for x in new_name.split(sv)) if sv else (new_name,)
books, new_id = func(item_id, new_names[0], self.backend)
affected_books.update(books)
id_map[item_id] = new_id if new_id != item_id:
moved_books.update(books)
if len(new_names) > 1:
# Add the extra items to the books extra = new_names[1:]
self._set_field(field, {book_id:self._fast_field_for(f, book_id) + extra for book_id in books})

if affected_books:
if field == 'authors':
self._set_field('author_sort',
{k:' & '.join(v) for k, v in self._author_sort_strings_for_books(affected_books).items()})
self._update_path(affected_books, mark_as_dirtied=False)
elif change_index and hasattr(f, 'index_field') and tweaks['series_index_auto_increment'] != 'no_change':
for book_id in moved_books:
self._set_field(f.index_field.name, {book_id:self._get_next_series_num_for(self._fast_field_for(f, book_id), field=field)})
self._mark_as_dirty(affected_books)
self._clear_link_map_cache(affected_books)
self.event_dispatcher(EventType.items_renamed, field, affected_books, id_map)
return affected_books, id_map

[문서]
@write_api def remove_items(self, field, item_ids, restrict_to_book_ids=None):
''' Delete all items in the specified field with the specified ids.
Returns the set of affected book ids. ``restrict_to_book_ids`` is an optional set of books ids. If specified the items will only be removed from those books. '''
field = self.fields[field]
if restrict_to_book_ids is not None and not isinstance(restrict_to_book_ids, (MutableSet, Set)):
restrict_to_book_ids = frozenset(restrict_to_book_ids)
affected_books = field.table.remove_items(item_ids, self.backend, restrict_to_book_ids=restrict_to_book_ids)
if affected_books:
if hasattr(field, 'index_field'):
self._set_field(field.index_field.name, {bid:1.0 for bid in affected_books})
else:
self._mark_as_dirty(affected_books)
self._clear_link_map_cache(affected_books)
self.event_dispatcher(EventType.items_removed, field, affected_books, item_ids)
return affected_books

[문서]
@write_api def add_custom_book_data(self, name, val_map, delete_first=False):
''' Add data for name where val_map is a map of book_ids to values. If delete_first is True, all previously stored data for name will be removed. '''
missing = frozenset(val_map) - self._all_book_ids()
if missing:
raise ValueError(f'add_custom_book_data: no such book_ids: {missing}')
self.backend.add_custom_data(name, val_map, delete_first)

[문서]
@read_api def get_custom_book_data(self, name, book_ids=(), default=None):
''' Get data for name. By default returns data for all book_ids, pass in a list of book ids if you only want some data. Returns a map of book_id to values. If a particular value could not be decoded, uses default for it. '''
return self.backend.get_custom_book_data(name, book_ids, default)

[문서]
@write_api def delete_custom_book_data(self, name, book_ids=()):
''' Delete data for name. By default deletes all data, if you only want to delete data for some book ids, pass in a list of book ids. '''
self.backend.delete_custom_book_data(name, book_ids)

[문서]
@read_api def get_ids_for_custom_book_data(self, name):
''' Return the set of book ids for which name has data. '''
return self.backend.get_ids_for_custom_book_data(name)

@read_api def conversion_options(self, book_id, fmt='PIPE'):
return self.backend.conversion_options(book_id, fmt)

@read_api def has_conversion_options(self, ids, fmt='PIPE'):
return self.backend.has_conversion_options(ids, fmt)

@write_api def delete_conversion_options(self, book_ids, fmt='PIPE'):
return self.backend.delete_conversion_options(book_ids, fmt)

[문서]
@write_api def set_conversion_options(self, options, fmt='PIPE'):
''' options must be a map of the form {book_id:conversion_options} '''
return self.backend.set_conversion_options(options, fmt)

@write_api def refresh_format_cache(self):
self.fields['formats'].table.read(self.backend)
self.format_metadata_cache.clear()

@write_api def refresh_ondevice(self):
self.fields['ondevice'].clear_caches()
self.clear_search_caches()
self.clear_composite_caches()

@read_api def books_matching_device_book(self, lpath):
ans = set()
for book_id, (_, _, _, _, lpaths) in self.fields['ondevice'].cache.items():
if lpath in lpaths:
ans.add(book_id)
return ans

[문서]
@read_api def tags_older_than(self, tag, delta=None, must_have_tag=None, must_have_authors=None):
'''
Return the ids of all books having the tag ``tag`` that are older than the specified time. tag comparison is case insensitive.

:param delta: A timedelta object or None. If None, then all ids with the tag are returned.

:param must_have_tag: If not None the list of matches will be restricted to books that have this tag

:param must_have_authors: A list of authors. If not None the list of matches will be restricted to books that have these authors (case insensitive).

'''
tag_map = {icu_lower(v):k for k, v in self._get_id_map('tags').items()}
tag = icu_lower(tag.strip())
mht = icu_lower(must_have_tag.strip()) if must_have_tag else None tag_id, mht_id = tag_map.get(tag, None), tag_map.get(mht, None)
ans = set()
if mht_id is None and mht:
return ans if tag_id is not None:
tagged_books = self._books_for_field('tags', tag_id)
if mht_id is not None and tagged_books:
tagged_books = tagged_books.intersection(self._books_for_field('tags', mht_id))
if tagged_books:
if must_have_authors is not None:
amap = {icu_lower(v):k for k, v in self._get_id_map('authors').items()}
books = None for author in must_have_authors:
abooks = self._books_for_field('authors', amap.get(icu_lower(author), None))
books = abooks if books is None else books.intersection(abooks)
if not books:
break tagged_books = tagged_books.intersection(books or set())
if delta is None:
ans = tagged_books else:
now = nowf()
for book_id in tagged_books:
ts = self._field_for('timestamp', book_id)
if (now - ts) > delta:
ans.add(book_id)
return ans@write_api def set_sort_for_authors(self, author_id_to_sort_map, update_books=True):
sort_map = self.fields['authors'].table.set_sort_names(author_id_to_sort_map, self.backend)
changed_books = set()
if update_books:
val_map = {}
for author_id in sort_map:
books = self._books_for_field('authors', author_id)
changed_books |= books for book_id in books:
authors = self._field_ids_for('authors', book_id)
adata = self._author_data(authors)
sorts = [adata[x]['sort'] for x in authors]
val_map[book_id] = ' & '.join(sorts)
if val_map:
self._set_field('author_sort', val_map)
if changed_books:
self._mark_as_dirty(changed_books)
self._clear_link_map_cache(changed_books)
return changed_books

@write_api def set_link_for_authors(self, author_id_to_link_map):
link_map = self.fields['authors'].table.set_links(author_id_to_link_map, self.backend)
changed_books = set()
for author_id in link_map:
changed_books |= self._books_for_field('authors', author_id)
if changed_books:
self._mark_as_dirty(changed_books)
self._clear_link_map_cache(changed_books)
self.event_dispatcher(EventType.links_changed, 'authors', frozenset(author_id_to_link_map))
return changed_books

@read_api def has_link_map(self, field):
return hasattr(getattr(self.fields.get(field), 'table', None), 'link_map')

[문서]
@read_api def get_link_map(self, for_field):
'''
Return a dictionary of links for the supplied field.

:param for_field: the lookup name of the field for which the link map is desired

:return: {field_value:link_value, ...} for non-empty links
'''
if for_field not in self.fields:
raise ValueError(f'Lookup name {for_field} is not a valid name')
table = self.fields[for_field].table lm = getattr(table, 'link_map', None)
if lm is None:
raise ValueError(f"Lookup name {for_field} doesn't have a link map")
lm = table.link_map vm = table.id_map ans = {vm.get(fid):v for fid,v in lm.items() if v}
ans.pop(None, None)
return ans

[문서]
@read_api def link_for(self, field, item_id):
'''
Return the link, if any, for the specified item or None if no link is found
'''
f = self.fields.get(field)
if f is not None:
table = f.table lm = getattr(table, 'link_map', None)
if lm is not None:
return lm.get(item_id)

[문서]
@read_api def get_all_link_maps_for_book(self, book_id):
'''
Returns all links for all fields referenced by book identified by book_id.
If book_id doesn't exist then the method returns {}.

Example: Assume author A has link X, author B has link Y, tag S has link
F, and tag T has link G. If book 1 has author A and tag T, this method returns {'authors':{'A':'X'}, 'tags':{'T', 'G'}}.
If book 2's author is neither A nor B and has no tags, this method returns {}.

:param book_id: the book id in question.

:return: {field: {field_value, link_value}, ... for all fields with a field_value having a non-empty link value for that book

'''
if not self._has_id(book_id):
# Works for book_id is None.
return {}
cached = self.link_maps_cache.get(book_id)
if cached is not None:
return cached links = {}
def add_links_for_field(f):
field_ids = self._field_ids_for(f, book_id)
if field_ids:
table = self.fields[f].table lm = table.link_map id_link_map = {fid:lm.get(fid) for fid in field_ids}
vm = table.id_map d = {vm.get(fid):v for fid, v in id_link_map.items() if v}
d.pop(None, None)
if d:
links[f] = d for field in ('authors', 'publisher', 'series', 'tags'):
add_links_for_field(field)
for field in self.field_metadata.custom_field_keys(include_composites=False):
if self._has_link_map(field):
add_links_for_field(field)
self.link_maps_cache[book_id] = links return links[문서]
@write_api def set_link_map(self, field, value_to_link_map, only_set_if_no_existing_link=False):
'''
Sets links for item values in field.
Note: this method doesn't change values not in the value_to_link_map

:param field: the lookup name
:param value_to_link_map: dict(field_value:link, ...). Note that these are values, not field ids.

:return: books changed by setting the link

'''
if field not in self.fields:
raise ValueError(f'Lookup name {field} is not a valid name')
table = getattr(self.fields[field], 'table', None)
if table is None:
raise ValueError(f"Lookup name {field} doesn't have a link map")
# Clear the links for book cache as we don't know what will be affected self.link_maps_cache = {}

fids = self._get_item_ids(field, value_to_link_map)
if only_set_if_no_existing_link:
lm = table.link_map id_to_link_map = {fid:value_to_link_map[k] for k, fid in fids.items() if fid is not None and not lm.get(fid)}
else:
id_to_link_map = {fid:value_to_link_map[k] for k, fid in fids.items() if fid is not None}
result_map = table.set_links(id_to_link_map, self.backend)
changed_books = set()
for id_ in result_map:
changed_books |= self._books_for_field(field, id_)
if changed_books:
self._mark_as_dirty(changed_books)
self._clear_link_map_cache(changed_books)
self.event_dispatcher(EventType.links_changed, field, frozenset(id_to_link_map))
return changed_books@read_api def lookup_by_uuid(self, uuid):
return self.fields['uuid'].table.lookup_by_uuid(uuid)

@write_api def delete_custom_column(self, label=None, num=None):
self.backend.delete_custom_column(label, num)

@write_api def create_custom_column(self, label, name, datatype, is_multiple, editable=True, display={}):
return self.backend.create_custom_column(label, name, datatype, is_multiple, editable=editable, display=display)

@write_api def set_custom_column_metadata(self, num, name=None, label=None, is_editable=None, display=None, update_last_modified=False):
changed = self.backend.set_custom_column_metadata(num, name=name, label=label, is_editable=is_editable, display=display)
if changed:
if update_last_modified:
self._update_last_modified(self._all_book_ids())
else:
self.backend.prefs.set('update_all_last_mod_dates_on_start', True)
return changed

@read_api def get_books_for_category(self, category, item_id_or_composite_value):
f = self.fields[category]
if hasattr(f, 'get_books_for_val'):
# Composite field return f.get_books_for_val(item_id_or_composite_value, self._get_proxy_metadata, self._all_book_ids())
return self._books_for_field(f.name, int(item_id_or_composite_value))

[문서]
@read_api def split_if_is_multiple_composite(self, f, val):
'''
If f is a composite column lookup key and the column is is_multiple then split v into unique non-empty values. The comparison is case sensitive.
Order is not preserved. Return a list() for compatibility with proxy metadata field getters, for example tags.
'''
fm = self.field_metadata.get(f, None)
if fm and fm['datatype'] == 'composite' and fm['is_multiple']:
sep = fm['is_multiple'].get('cache_to_list', ',')
return list({v.strip() for v in val.split(sep) if v.strip()})
return val

[문서]
@read_api def data_for_find_identical_books(self):
''' Return data that can be used to implement
:meth:`find_identical_books` in a worker process without access to the db. See db.utils for an implementation. '''
at = self.fields['authors'].table author_map = defaultdict(set)
for aid, author in at.id_map.items():
author_map[icu_lower(author)].add(aid)
return (author_map, at.col_book_map.copy(), self.fields['title'].table.book_col_map.copy(), self.fields['languages'].book_value_map.copy())

@read_api def update_data_for_find_identical_books(self, book_id, data):
author_map, author_book_map, title_map, lang_map = data title_map[book_id] = self._field_for('title', book_id)
lang_map[book_id] = self._field_for('languages', book_id)
at = self.fields['authors'].table for aid in at.book_col_map.get(book_id, ()):
author_map[icu_lower(at.id_map[aid])].add(aid)
try:
author_book_map[aid].add(book_id)
except KeyError:
author_book_map[aid] = {book_id}

[문서]
@read_api def find_identical_books(self, mi, search_restriction='', book_ids=None):
''' Finds books that have a superset of the authors in mi and the same title (title is fuzzy matched). See also :meth:`data_for_find_identical_books`. '''
from calibre.db.utils import fuzzy_title identical_book_ids = set()
langq = tuple(x for x in map(canonicalize_lang, mi.languages or ()) if x and x != 'und')
if mi.authors:
try:
quathors = mi.authors[:20] # Too many authors causes parsing of the search expression to fail query = ' and '.join('authors:"={}"'.format(a.replace('"', '')) for a in quathors)
qauthors = mi.authors[20:]
except ValueError:
return identical_book_ids try:
book_ids = self._search(query, restriction=search_restriction, book_ids=book_ids)
except Exception:
traceback.print_exc()
return identical_book_ids if qauthors and book_ids:
matches = set()
qauthors = {icu_lower(x) for x in qauthors}
for book_id in book_ids:
aut = self._field_for('authors', book_id)
if aut:
aut = {icu_lower(x) for x in aut}
if aut.issuperset(qauthors):
matches.add(book_id)
book_ids = matches for book_id in book_ids:
fbook_title = self._field_for('title', book_id)
fbook_title = fuzzy_title(fbook_title)
mbook_title = fuzzy_title(mi.title)
if fbook_title == mbook_title:
bl = self._field_for('languages', book_id)
if not langq or not bl or bl == langq:
identical_book_ids.add(book_id)
return identical_book_ids@read_api def get_top_level_move_items(self):
all_paths = {self._get_book_path(book_id, sep='/').partition('/')[0] for book_id in self._all_book_ids()}
return self.backend.get_top_level_move_items(all_paths)

@write_api def move_library_to(self, newloc, progress=None, abort=None):
def progress_callback(item_name, item_count, total):
try:
if progress is not None:
progress(item_name, item_count, total)
except Exception:
traceback.print_exc()

all_paths = {self._get_book_path(book_id, sep='/').partition('/')[0] for book_id in self._all_book_ids()}
self.backend.move_library_to(all_paths, newloc, progress=progress_callback, abort=abort)

@read_api def saved_search_names(self):
return self._search_api.saved_searches.names()

@read_api def saved_search_lookup(self, name):
return self._search_api.saved_searches.lookup(name)

@write_api def saved_search_set_all(self, smap):
self._search_api.saved_searches.set_all(smap)
self._clear_search_caches()

@write_api def saved_search_delete(self, name):
self._search_api.saved_searches.delete(name)
self._clear_search_caches()

@write_api def saved_search_add(self, name, val):
self._search_api.saved_searches.add(name, val)

@write_api def saved_search_rename(self, old_name, new_name):
self._search_api.saved_searches.rename(old_name, new_name)
self._clear_search_caches()

@write_api def change_search_locations(self, newlocs):
self._search_api.change_locations(newlocs)

@write_api def refresh_search_locations(self):
self._search_api.change_locations(self.field_metadata.get_search_terms())

@write_api def dump_and_restore(self, callback=None, sql=None):
return self.backend.dump_and_restore(callback=callback, sql=sql)

@write_api def vacuum(self, include_fts_db=False, include_notes_db=True, rebuild_annotations_fts=False):
self.is_doing_rebuild_or_vacuum = True try:
self.backend.vacuum(include_fts_db, include_notes_db, rebuild_annotations_fts)
finally:
self.is_doing_rebuild_or_vacuum = False def __del__(self):
self.close()

def _shutdown_fts(self, stage=1):
if stage == 1:
self.backend.shutdown_fts()
if self.fts_queue_thread is not None:
self.fts_job_queue.shutdown(True)
if hasattr(self, 'fts_dispatch_stop_event'):
self.fts_dispatch_stop_event.set()
return
# the fts supervisor thread could be in the middle of committing a
# result to the db, so holding a lock here will cause a deadlock if self.fts_queue_thread is not None:
t, self.fts_queue_thread = self.fts_queue_thread, None if not sys.is_finalizing():
t.join()
self.backend.join_fts()

@api def close(self):
with self.write_lock:
if hasattr(self, 'close_called'):
return self.close_called = True self.shutting_down = True m, self.maintain_page_counts = self.maintain_page_counts, None m.shutdown()
self.event_dispatcher.close()
self._shutdown_fts()
try:
from calibre.customize.ui import available_library_closed_plugins except ImportError:
pass # happens during interpreter shutdown else:
for plugin in available_library_closed_plugins():
try:
plugin.run(self)
except Exception:
traceback.print_exc()
self._shutdown_fts(stage=2)
if m is not None:
m.wait_for_worker_shutdown()
with self.write_lock:
self.backend.close()

@property def is_closed(self):
return self.backend.is_closed

@write_api def clear_trash_bin(self):
self.backend.clear_trash_dir()

@read_api def list_trash_entries(self):
books, formats = self.backend.list_trash_entries()
ff = []
for e in formats:
if self._has_id(e.book_id):
ff.append(e)
e.cover_path = self.format_abspath(e.book_id, '__COVER_INTERNAL__')
return books, formats

@read_api def copy_format_from_trash(self, book_id, fmt, dest):
fmt = fmt.upper()
fpath = self.backend.path_for_trash_format(book_id, fmt)
if not fpath:
raise ValueError(f'No format {fmt} found in book {book_id}')
shutil.copyfile(fpath, dest)

[문서]
@write_api def move_format_from_trash(self, book_id, fmt):
''' Undelete a format from the trash directory '''
if not self._has_id(book_id):
raise ValueError(f'A book with the id {book_id} does not exist')
fmt = fmt.upper()
try:
name = self.fields['formats'].format_fname(book_id, fmt)
except Exception:
name = None fpath = self.backend.path_for_trash_format(book_id, fmt)
if not fpath:
raise ValueError(f'No format {fmt} found in book {book_id}')
size, fname = self._do_add_format(book_id, fmt, fpath, name)
self.format_metadata_cache.pop(book_id, None)
max_size = self.fields['formats'].table.update_fmt(book_id, fmt, fname, size, self.backend)
self.fields['size'].table.update_sizes({book_id: max_size})
self._queue_pages_scan(book_id)
self.event_dispatcher(EventType.format_added, book_id, fmt)
self.backend.remove_trash_formats_dir_if_empty(book_id)

@read_api def copy_book_from_trash(self, book_id, dest: str):
self.backend.copy_book_from_trash(book_id, dest)

[문서]
@write_api def move_book_from_trash(self, book_id):
''' Undelete a book from the trash directory '''
if self._has_id(book_id):
raise ValueError(f'A book with the id {book_id} already exists')
mi, annotations, formats = self.backend.get_metadata_for_trash_book(book_id)
mi.cover = None self._create_book_entry(mi, add_duplicates=True, force_id=book_id, apply_import_tags=False, preserve_uuid=True)
path = self._get_book_path(book_id)
self.backend.move_book_from_trash(book_id, path)
self.format_metadata_cache.pop(book_id, None)
f = self.fields['formats'].table max_size = 0 for (fmt, size, fname) in formats:
max_size = max(max_size, f.update_fmt(book_id, fmt, fname, size, self.backend))
self.fields['size'].table.update_sizes({book_id: max_size})
cover = self.backend.cover_abspath(book_id, path)
if cover and os.path.exists(cover):
self._set_field('cover', {book_id:1})
if annotations:
self._restore_annotations(book_id, annotations)
self._queue_pages_scan(book_id)

[문서]
@write_api def delete_trash_entry(self, book_id, category):
" Delete an entry from the trash. Here category is 'b' for books and 'f' for formats. "
self.backend.delete_trash_entry(book_id, category)

[문서]
@write_api def expire_old_trash(self):
' Expire entries from the trash that are too old '
self.backend.expire_old_trash()

[문서]
@write_api def restore_book(self, book_id, mi, last_modified, path, formats, annotations=()):
''' Restore the book entry in the database for a book that already exists on the filesystem '''
cover, mi.cover = mi.cover, None self._create_book_entry(mi, add_duplicates=True, force_id=book_id, apply_import_tags=False, preserve_uuid=True)
self._update_last_modified((book_id,), last_modified)
if cover and os.path.exists(cover):
self._set_field('cover', {book_id:1})
f = self.fields['formats'].table for (fmt, size, fname) in formats:
f.update_fmt(book_id, fmt, fname, size, self.backend)
self.fields['path'].table.set_path(book_id, path, self.backend)
if annotations:
self._restore_annotations(book_id, annotations)
self._queue_pages_scan(book_id)

@read_api def virtual_libraries_for_books(self, book_ids, virtual_fields=None):
# use a primitive lock to ensure that only one thread is updating
# the cache and that recursive calls don't do the update. This
# method can recurse via self._search()
with try_lock(self.vls_cache_lock) as got_lock:
# Using a list is slightly faster than a set.
c = defaultdict(list)
if not got_lock:
# We get here if resolving the books in a VL triggers another VL
# cache calculation. This can be 'real' recursion, for example a
# VL expression using a template that calls virtual_libraries(),
# or a search using a location of 'all' that causes evaluation
# of a composite that uses virtual_libraries(). The first case
# is an error and the exception message should appear somewhere.
# However, the error can seem nondeterministic. It might not be
# raised if the use is via a composite and that composite is
# evaluated before it is used in the search. The second case is
# also an error but if the composite isn't used in a VL then the
# eventual answer will be correct because get_metadata() will
# clear the caches.
raise ValueError(_('Recursion detected while processing Virtual library "%s"')
% self.vls_for_books_lib_in_process)
if self.vls_for_books_cache is None:
libraries = self._pref('virtual_libraries', {})
for lib, expr in libraries.items():
book = None self.vls_for_books_lib_in_process = lib try:
for book in self._search(expr, virtual_fields=virtual_fields):
c[book].append(lib)
except Exception as e:
if book:
c[book].append(_('[Error in Virtual library {0}: {1}]').format(lib, str(e)))
self.vls_for_books_cache = {b:tuple(sorted(libs, key=sort_key)) for b, libs in c.items()}
if not book_ids:
book_ids = self._all_book_ids()
# book_ids is usually 1 long. The loop will be faster than a comprehension r = {}
default = ()
for b in book_ids:
r[b] = self.vls_for_books_cache.get(b, default)
return r

[문서]
@read_api def user_categories_for_books(self, book_ids, proxy_metadata_map=None):
''' Return the user categories for the specified books.
proxy_metadata_map is optional and is useful for a performance boost, in contexts where a ProxyMetadata object for the books already exists.
It should be a mapping of book_ids to their corresponding ProxyMetadata objects.
'''
user_cats = self._pref('user_categories', {})
pmm = proxy_metadata_map or {}
ans = {}

for book_id in book_ids:
proxy_metadata = pmm.get(book_id) or self._get_proxy_metadata(book_id)
user_cat_vals = ans[book_id] = {}
for ucat, categories in user_cats.items():
user_cat_vals[ucat] = res = []
for name, cat, ign in categories:
try:
field_obj = self.fields[cat]
except KeyError:
continue if field_obj.is_composite:
v = field_obj.get_value_with_cache(book_id, lambda x:proxy_metadata)
else:
v = self._fast_field_for(field_obj, book_id)

if isinstance(v, (list, tuple)):
if name in v:
res.append([name, cat])
elif name == v:
res.append([name, cat])
return ans

[문서]
@write_api def embed_metadata(self, book_ids, only_fmts=None, report_error=None, report_progress=None):
''' Update metadata in all formats of the specified book_ids to current metadata in the database. '''
field = self.fields['formats']
from calibre.customize.ui import apply_null_metadata from calibre.ebooks.metadata.meta import set_metadata from calibre.ebooks.metadata.opf2 import pretty_print if only_fmts:
only_fmts = {f.lower() for f in only_fmts}

def doit(fmt, mi, stream):
with apply_null_metadata, pretty_print:
set_metadata(stream, mi, stream_type=fmt, report_error=report_error)
stream.seek(0, os.SEEK_END)
return stream.tell()

for i, book_id in enumerate(book_ids):
fmts = field.table.book_col_map.get(book_id, ())
if not fmts:
continue mi = self._get_metadata(book_id)
buf = BytesIO()
if not self._copy_cover_to(book_id, buf):
return cdata = buf.getvalue()
if cdata:
mi.cover_data = ('jpeg', cdata)
try:
path = self._get_book_path(book_id)
except Exception:
continue for fmt in fmts:
if only_fmts is not None and fmt.lower() not in only_fmts:
continue try:
name = self.fields['formats'].format_fname(book_id, fmt)
except Exception:
continue if name and path:
try:
new_size = self.backend.apply_to_format(book_id, path, name, fmt, partial(doit, fmt, mi))
except Exception as e:
if report_error is not None:
tb = traceback.format_exc()
if iswindows and isinstance(e, PermissionError) and e.filename and isinstance(e.filename, str):
from calibre_extensions import winutil try:
p = winutil.get_processes_using_files(e.filename)
except OSError:
pass else:
path_map = {x['path']: x for x in p}
tb = _('Could not open the file: "{}". It is already opened in the following programs:').format(e.filename)
for path, x in path_map.items():
tb += '\n' + f'{x["app_name"]}: {path}'
report_error(mi, fmt, tb)
new_size = None else:
raise if new_size is not None:
self.format_metadata_cache[book_id].get(fmt, {})['size'] = new_size max_size = self.fields['formats'].table.update_fmt(book_id, fmt, name, new_size, self.backend)
self.fields['size'].table.update_sizes({book_id: max_size})
if report_progress is not None:
report_progress(i+1, len(book_ids), mi)@read_api def get_last_read_positions(self, book_id, fmt='', user='', order_by='', limit=0):
q = 'SELECT device,cfi,epoch,pos_frac,format,user FROM last_read_positions WHERE book=?'
bindings = [book_id]
if fmt:
q += ' AND format=?'
bindings.append(fmt.upper())
if user:
q += ' AND user=?'
bindings.append(user)
if order_by in ('pos_frac', 'epoch'):
q += f' ORDER BY {order_by} DESC'
if limit:
q += f' LIMIT {int(limit)}'
return tuple({'device':device, 'cfi': cfi, 'epoch':epoch, 'pos_frac':pos_frac, 'format': format, 'user': user}
for device, cfi, epoch, pos_frac, format, user in self.backend.execute(q, tuple(bindings)))

@write_api def set_last_read_position(self, book_id, fmt, user='_', device='_', cfi=None, epoch=None, pos_frac=0):
self.backend.set_last_read_position(book_id, fmt, user, device, cfi, epoch, pos_frac)

@write_api # doesn't need write access but sqlite does require only a single thread to access the db during backup def export_library(self, library_key, exporter, progress=None, abort=None):
from polyglot.binary import as_hex_unicode key_prefix = as_hex_unicode(library_key)
book_ids = self._all_book_ids()
total = len(book_ids) + 2 has_fts = self.is_fts_enabled()
if has_fts:
total += 1 poff = 0 def report_progress(fname):
nonlocal poff if progress is not None:
progress(fname, poff, total)
poff += 1

@contextmanager def tempfile_for_export(which: str) -> Iterator[str]:
import tempfile fd, ans = tempfile.mkstemp(suffix=f'-{which}.db', dir=exporter.base)
os.close(fd)
try:
yield ans finally:
os.remove(ans)

report_progress('metadata.db')
with tempfile_for_export('metadata') as tf:
self.backend.backup_database(tf)
dbkey = key_prefix + ':::' + 'metadata.db'
with open(tf, 'rb') as f:
exporter.add_file(f, dbkey)
if has_fts:
report_progress('full-text-search.db')
with tempfile_for_export('fts') as tf:
self.backend.backup_fts_database(tf)
ftsdbkey = key_prefix + ':::full-text-search.db'
with open(tf, 'rb') as f:
exporter.add_file(f, ftsdbkey)
with tempfile_for_export('notes') as tf, open(tf, 'r+b') as pt:
self.backend.export_notes_data(pt)
pt.flush()
pt.seek(0)
report_progress('notes.db')
notesdbkey = key_prefix + ':::notes.db'
exporter.add_file(pt, notesdbkey)

format_metadata = {}
extra_files = {}
metadata = {'format_data':format_metadata, 'metadata.db':dbkey, 'notes.db': notesdbkey, 'total':total, 'extra_files': extra_files}
if has_fts:
metadata['full-text-search.db'] = ftsdbkey for i, book_id in enumerate(book_ids):
if abort is not None and abort.is_set():
return if progress is not None:
report_progress(self._field_for('title', book_id))
format_metadata[book_id] = fm = {}
for fmt in self._formats(book_id):
mdata = self.format_metadata(book_id, fmt)
key = f'{key_prefix}:{book_id}:{fmt}'
fm[fmt] = key mtime = mdata.get('mtime')
if mtime is not None:
mtime = timestampfromdt(mtime)
with exporter.start_file(key, mtime=mtime) as dest:
self._copy_format_to(book_id, fmt, dest)
cover_key = '{}:{}:{}'.format(key_prefix, book_id, '.cover')
with exporter.start_file(cover_key) as dest:
if not self.copy_cover_to(book_id, dest):
dest.discard()
else:
fm['.cover'] = cover_key bp = self._get_book_path(book_id, sep='/', unsafe=True)
extra_files[book_id] = ef = {}
if bp:
for (relpath, fobj, stat_result) in self.backend.iter_extra_files(book_id, bp, self.fields['formats']):
key = f'{key_prefix}:{book_id}:.|{relpath}'
with exporter.start_file(key, mtime=stat_result.st_mtime) as dest:
shutil.copyfileobj(fobj, dest)
ef[relpath] = key exporter.set_metadata(library_key, metadata)
if progress is not None:
progress(_('Completed'), total, total)

[문서]
@read_api def annotations_map_for_book(self, book_id, fmt, user_type='local', user='viewer'):
'''
Return a map of annotation type -> annotation data for the specified book_id, format, user and user_type.
'''
ans = {}
for annot in self.backend.annotations_for_book(book_id, fmt, user_type, user):
ans.setdefault(annot['type'], []).append(annot)
return ans

[문서]
@read_api def all_annotations_for_book(self, book_id):
'''
Return a tuple containing all annotations for the specified book_id as a dict with keys:
`format`, `user_type`, `user`, `annotation`. Here, annotation is the annotation data.
'''
return tuple(self.backend.all_annotations_for_book(book_id))

[문서]
@read_api def annotation_count_for_book(self, book_id):
'''
Return the number of annotations for the specified book available in the database.
'''
return self.backend.annotation_count_for_book(book_id)

[문서]
@read_api def all_annotation_users(self):
'''
Return a tuple of all (user_type, user name) that have annotations.
'''
return tuple(self.backend.all_annotation_users())

[문서]
@read_api def all_annotation_types(self):
'''
Return a tuple of all annotation types in the database.
'''
return tuple(self.backend.all_annotation_types())

[문서]
@read_api def all_annotations( self, restrict_to_user=None, limit=None, annotation_type=None, annotation_style=None, ignore_removed=False, restrict_to_book_ids=None,
):
'''
Return a tuple of all annotations matching the specified criteria.
`ignore_removed` controls whether removed (deleted) annotations are also returned.
Removed annotations are just a skeleton used for merging of annotations.
'''
return tuple(self.backend.all_annotations(restrict_to_user, limit, annotation_type, annotation_style, ignore_removed, restrict_to_book_ids))

[문서]
@read_api def all_annotation_styles(self):
'''
Return a tuple of all built-in annotation styles.
'''
return self.backend.all_annotation_styles()

[문서]
@read_api def search_annotations( self, fts_engine_query, use_stemming=True, highlight_start=None, highlight_end=None, snippet_size=None, annotation_type=None, restrict_to_book_ids=None, restrict_to_user=None, ignore_removed=False, annotation_style=None,
):
'''
Return of a tuple of annotations matching the specified Full-text query.
'''
return tuple(self.backend.search_annotations( fts_engine_query, use_stemming, highlight_start, highlight_end, snippet_size, annotation_type, restrict_to_book_ids, restrict_to_user, ignore_removed, annotation_style,
))

[문서]
@write_api def delete_annotations(self, annot_ids):
'''
Delete annotations with the specified ids.
'''
self.backend.delete_annotations(annot_ids)

[문서]
@write_api def update_annotations(self, annot_id_map):
'''
Update annotations.
'''
self.backend.update_annotations(annot_id_map)

@write_api def restore_annotations(self, book_id, annotations):
from calibre.utils.date import EPOCH from calibre.utils.iso8601 import parse_iso8601 umap = defaultdict(list)
for adata in annotations:
key = adata['user_type'], adata['user'], adata['format']
a = adata['annotation']
ts = (parse_iso8601(a['timestamp']) - EPOCH).total_seconds()
umap[key].append((a, ts))
for (user_type, user, fmt), annots_list in umap.items():
self._set_annotations_for_book(book_id, fmt, annots_list, user_type=user_type, user=user)

[문서]
@write_api def set_annotations_for_book(self, book_id, fmt, annots_list, user_type='local', user='viewer'):
'''
Set all annotations for the specified book_id, fmt, user_type and user.
'''
self.backend.set_annotations_for_book(book_id, fmt, annots_list, user_type, user)

[문서]
@write_api def merge_annotations_for_book(self, book_id, fmt, annots_list, user_type='local', user='viewer'):
'''
Merge the specified annotations into the existing annotations for book_id, fm, user_type, and user.
'''
from calibre.utils.date import EPOCH from calibre.utils.iso8601 import parse_iso8601 amap = self._annotations_map_for_book(book_id, fmt, user_type=user_type, user=user)
merge_annotations(annots_list, amap)
alist = []
for val in amap.values():
for annot in val:
ts = (parse_iso8601(annot['timestamp']) - EPOCH).total_seconds()
alist.append((annot, ts))
self._set_annotations_for_book(book_id, fmt, alist, user_type=user_type, user=user)

@write_api def save_annotations_list(self, book_id: int, book_fmt: str, sync_annots_user: str, alist: list[dict]) -> None:
self.backend.save_annotations_list(book_id, book_fmt, sync_annots_user, alist)

@write_api def reindex_annotations(self):
self.backend.reindex_annotations()

@read_api def are_paths_inside_book_dir(self, book_id, paths, sub_path=''):
try:
path = self._get_book_path(book_id)
except Exception:
return set()
return {x for x in paths if self.backend.is_path_inside_book_dir(x, path, sub_path)}

[문서]
@write_api def add_extra_files(self, book_id, map_of_relpath_to_stream_or_path, replace=True, auto_rename=False):
' Add extra data files '
path = self._get_book_path(book_id)
added = {}
for relpath, stream_or_path in map_of_relpath_to_stream_or_path.items():
added[relpath] = bool(self.backend.add_extra_file(relpath, stream_or_path, path, replace, auto_rename))
self._clear_extra_files_cache(book_id)
return added

[문서]
@write_api def rename_extra_files(self, book_id, map_of_relpath_to_new_relpath, replace=False):
' Rename extra data files '
path = self._get_book_path(book_id)
renamed = set()
for relpath, newrelpath in map_of_relpath_to_new_relpath.items():
if self.backend.rename_extra_file(relpath, newrelpath, path, replace):
renamed.add(relpath)
self._clear_extra_files_cache(book_id)
return renamed

[문서]
@write_api def merge_extra_files(self, dest_id, src_ids, replace=False):
' Merge the extra files from src_ids into dest_id. Conflicting files are auto-renamed unless replace=True in which case they are replaced. '
added = set()
path = self._field_for('path', dest_id)
if path:
path = path.replace('/', os.sep)
for src_id in src_ids:
book_path = self._field_for('path', src_id)
if book_path:
book_path = book_path.replace('/', os.sep)
for (relpath, file_path, stat_result) in self.backend.iter_extra_files( src_id, book_path, self.fields['formats'], yield_paths=True):
added.add(self.backend.add_extra_file(relpath, file_path, path, replace=replace, auto_rename=True))
self._clear_extra_files_cache(dest_id)
return added

[문서]
@write_api def remove_extra_files(self, book_id: int, relpaths: Iterable[str], permanent=False) -> dict[str, Exception | None]:
'''
Delete the specified extra files, either to Recycle Bin or permanently.
'''
path = self._get_book_path(book_id, sep='/', unsafe=True)
if path:
self._clear_extra_files_cache(book_id)
return self.backend.remove_extra_files(path, relpaths, permanent)
return dict.fromkeys(relpaths)

[문서]
@read_api def list_extra_files(self, book_id, use_cache=False, pattern='') -> tuple[ExtraFile, ...]:
'''
Get information about extra files in the book's directory.

:param book_id: the database book id for the book
:param pattern: the pattern of filenames to search for. Empty pattern matches all extra files. Patterns must use / as separator.
Use the DATA_FILE_PATTERN constant to match files inside the data directory.

:return: A tuple of all extra files matching the specified pattern. Each element of the tuple is
ExtraFile(relpath, file_path, stat_result). Where relpath is the relative path of the file to the book directory using / as a separator.
stat_result is the result of calling os.stat() on the file.
'''
ans = self.extra_files_cache.setdefault(book_id, {}).get(pattern)
if ans is None or not use_cache:
ans = []
path = self._get_book_path(book_id, sep='/', unsafe=True)
if path:
for (relpath, file_path, stat_result) in self.backend.iter_extra_files( book_id, path, self.fields['formats'], yield_paths=True, pattern=pattern
):
ans.append(ExtraFile(relpath, file_path, stat_result))
self.extra_files_cache[book_id][pattern] = ans = tuple(ans)
return ans

@read_api def copy_extra_file_to(self, book_id, relpath, stream_or_path):
path = self._get_book_path(book_id)
self.backend.copy_extra_file_to(book_id, path, relpath, stream_or_path)

@write_api def merge_book_metadata(self, dest_id, src_ids, replace_cover=False, save_alternate_cover=False):
dest_mi = self.get_metadata(dest_id)
merged_identifiers = self._field_for('identifiers', dest_id) or {}
orig_dest_comments = dest_mi.comments dest_cover = orig_dest_cover = self.cover(dest_id)
had_orig_cover = bool(dest_cover)
alternate_covers = []
from calibre.utils.date import is_date_undefined def is_null_date(x):
return x is None or is_date_undefined(x)

for src_id in src_ids:
src_mi = self.get_metadata(src_id)

if src_mi.comments and orig_dest_comments != src_mi.comments:
if not dest_mi.comments:
dest_mi.comments = src_mi.comments else:
dest_mi.comments = str(dest_mi.comments) + '\n\n' + str(src_mi.comments)
if src_mi.title and dest_mi.is_null('title'):
dest_mi.title = src_mi.title dest_mi.title_sort = src_mi.title_sort if (src_mi.authors and src_mi.authors[0] != _('Unknown')) and (not dest_mi.authors or dest_mi.authors[0] == _('Unknown')):
dest_mi.authors = src_mi.authors dest_mi.author_sort = src_mi.author_sort if src_mi.tags:
if not dest_mi.tags:
dest_mi.tags = src_mi.tags else:
dest_mi.tags.extend(src_mi.tags)
if not dest_cover or replace_cover:
src_cover = self.cover(src_id)
if src_cover:
if save_alternate_cover and dest_cover:
alternate_covers.append(dest_cover)
dest_cover = src_cover replace_cover = False elif save_alternate_cover:
src_cover = self.cover(src_id)
if src_cover:
alternate_covers.append(src_cover)
if not dest_mi.publisher:
dest_mi.publisher = src_mi.publisher if not dest_mi.rating:
dest_mi.rating = src_mi.rating if not dest_mi.series:
dest_mi.series = src_mi.series dest_mi.series_index = src_mi.series_index if is_null_date(dest_mi.pubdate) and not is_null_date(src_mi.pubdate):
dest_mi.pubdate = src_mi.pubdate src_identifiers = (src_mi.get_identifiers() or {}).copy()
src_identifiers.update(merged_identifiers)
merged_identifiers = src_identifiers.copy()

if merged_identifiers:
dest_mi.set_identifiers(merged_identifiers)
self._set_metadata(dest_id, dest_mi, ignore_errors=False)

if dest_cover and (not had_orig_cover or dest_cover is not orig_dest_cover):
self._set_cover({dest_id: dest_cover})
if alternate_covers:
existing = {x[0] for x in self._list_extra_files(dest_id)}
h, ext = os.path.splitext(COVER_FILE_NAME)
template = f'{DATA_DIR_NAME}/{h}-{{:03d}}{ext}'
for cdata in alternate_covers:
for i in range(1, 1000):
q = template.format(i)
if q not in existing:
existing.add(q)
self._add_extra_files(dest_id, {q: BytesIO(cdata)}, replace=False, auto_rename=True)
break for key in self.field_metadata: # loop through all defined fields fm = self.field_metadata[key]
if not fm['is_custom']:
continue dt = fm['datatype']
label = fm['label']
try:
field = self.field_metadata.label_to_key(label)
except ValueError:
continue
# Get orig_dest_comments before it gets changed if dt == 'comments':
orig_dest_value = self._field_for(field, dest_id)

for src_id in src_ids:
dest_value = self._field_for(field, dest_id)
src_value = self._field_for(field, src_id)
if (dt == 'comments' and src_value and src_value != orig_dest_value):
if not dest_value:
self._set_field(field, {dest_id: src_value})
else:
dest_value = str(dest_value) + '\n\n' + str(src_value)
self._set_field(field, {dest_id: dest_value})
if (dt in {'bool', 'int', 'float', 'rating', 'datetime'} and dest_value is None):
self._set_field(field, {dest_id: src_value})
if (dt == 'series' and not dest_value and src_value):
src_index = self._field_for(field + '_index', src_id)
self._set_field(field, {dest_id:src_value})
self._set_field(field + '_index', {dest_id:src_index})
if ((dt == 'enumeration' or (dt == 'text' and not fm['is_multiple'])) and not dest_value):
self._set_field(field, {dest_id:src_value})
if (dt == 'text' and fm['is_multiple'] and src_value):
if not dest_value:
dest_value = src_value else:
dest_value = list(dest_value)
dest_value.extend(src_value)
self._set_field(field, {dest_id: dest_value})

@read_api def clone_for_readonly_access(self, dest_dir: str) -> str:
return self.backend.clone_for_readonly_access(dest_dir)def import_library(library_key, importer, library_path, progress=None, abort=None):
from calibre.db.backend import DB metadata = importer.metadata[library_key]
total = metadata['total']
poff = 0 def report_progress(fname):
nonlocal poff if progress is not None:
progress(fname, poff, total)
poff += 1 report_progress('metadata.db')
if abort is not None and abort.is_set():
return importer.save_file(metadata['metadata.db'], 'metadata.db for ' + library_path, os.path.join(library_path, 'metadata.db'))
if 'full-text-search.db' in metadata:
if progress is not None:
progress('full-text-search.db', 1, total)
if abort is not None and abort.is_set():
return poff += 1 importer.save_file(metadata['full-text-search.db'], 'full-text-search.db for ' + library_path, os.path.join(library_path, 'full-text-search.db'))
if abort is not None and abort.is_set():
return if 'notes.db' in metadata:
import zipfile notes_dir = os.path.join(library_path, NOTES_DIR_NAME)
os.makedirs(notes_dir, exist_ok=True)
with importer.start_file(metadata['notes.db'], 'notes.db for ' + library_path) as stream:
stream.check_hash = False with zipfile.ZipFile(stream) as zf:
for zi in zf.infolist():
tpath = zf._extract_member(zi, notes_dir, None)
date_time = mktime(zi.date_time + (0, 0, -1))
os.utime(tpath, (date_time, date_time))
if abort is not None and abort.is_set():
return if importer.corrupted_files:
raise ValueError('Corrupted files:\n' + '\n'.join(importer.corrupted_files))
cache = Cache(DB(library_path, load_user_formatter_functions=False))
cache.init()

format_data = {int(book_id):data for book_id, data in metadata['format_data'].items()}
extra_files = {int(book_id):data for book_id, data in metadata.get('extra_files', {}).items()}
for i, (book_id, fmt_key_map) in enumerate(format_data.items()):
if abort is not None and abort.is_set():
return title = cache._field_for('title', book_id)
if progress is not None:
progress(title, i + poff, total)
cache._update_path((book_id,), mark_as_dirtied=False)
for fmt, fmtkey in fmt_key_map.items():
if fmt == '.cover':
with importer.start_file(fmtkey, _('Cover for %s') % title) as stream:
path = cache._get_book_path(book_id)
cache.backend.set_cover(book_id, path, stream, no_processing=True)
else:
with importer.start_file(fmtkey, _('{0} format for {1}').format(fmt.upper(), title)) as stream:
size, fname = cache._do_add_format(book_id, fmt, stream, mtime=stream.mtime)
cache.fields['formats'].table.update_fmt(book_id, fmt, fname, size, cache.backend)
for relpath, efkey in extra_files.get(book_id, {}).items():
with importer.start_file(efkey, _('Extra file {0} for book {1}').format(relpath, title)) as stream:
path = cache._get_book_path(book_id)
cache.backend.add_extra_file(relpath, stream, path)
cache.dump_metadata({book_id})
if importer.corrupted_files:
raise ValueError('Corrupted files:\n' + '\n'.join(importer.corrupted_files))
if progress is not None:
progress(_('Completed'), total, total)
return cache
# }}}

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 시작 »
- 모듈 코드 »
- calibre.devices.interface

calibre.devices.interface의 소스 코드

__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os from collections import namedtuple from typing import NamedTuple from calibre import prints from calibre.constants import iswindows from calibre.customize import Plugin

FAKE_DEVICE_SERIAL = '__fake_device_for_use_with_connect_to_folder__:'

class ModelMetadata(NamedTuple):
manufacturer_name: str model_name: str vendor_id: int product_id: int bcd: int driver_class: type

@property def settings_key(self) -> str:
return f'{self.manufacturer_name} - {self.model_name}'

def detected_device(self, folder_path):
from calibre.devices.scanner import USBDevice return USBDevice(self.vendor_id, self.product_id, self.bcd, self.manufacturer_name, self.model_name, FAKE_DEVICE_SERIAL + folder_path)

class OpenPopupMessage:

def __init__(self, title='', message='', level='info', skip_dialog_skip_precheck=True):
self.title = title self.message = message self.level = level self.skip_dialog_skip_precheck = skip_dialog_skip_precheck

[문서]
class DevicePlugin(Plugin):
'''
Defines the interface that should be implemented by backends that communicate with an e-book reader.
'''
type = _('Device interface')

#: Ordered list of supported formats
FORMATS = ['lrf', 'rtf', 'pdf', 'txt']
# If True, the config dialog will not show the formats box
HIDE_FORMATS_CONFIG_BOX = False

#: VENDOR_ID can be either an integer, a list of integers or a dictionary
#: If it is a dictionary, it must be a dictionary of dictionaries,
#: of the form::
#:
#: {
#: integer_vendor_id : { product_id : [list of BCDs], ... },
#: ...
#: }
#:
VENDOR_ID = 0x0000

#: An integer or a list of integers
PRODUCT_ID = 0x0000
#: BCD can be either None to not distinguish between devices based on BCD, or
#: it can be a list of the BCD numbers of all devices supported by this driver.
BCD = None

#: Height for thumbnails on the device
THUMBNAIL_HEIGHT = 68

#: Compression quality for thumbnails. Set this closer to 100 to have better
#: quality thumbnails with fewer compression artifacts. Of course, the
#: thumbnails get larger as well.
THUMBNAIL_COMPRESSION_QUALITY = 75

#: Set this to True if the device supports updating cover thumbnails during
#: sync_booklists. Setting it to true will ask device.py to refresh the
#: cover thumbnails during book matching
WANTS_UPDATED_THUMBNAILS = False

#: Whether the metadata on books can be set via the GUI.
CAN_SET_METADATA = ['title', 'authors', 'collections']

#: Whether the device can handle device_db metadata plugboards
CAN_DO_DEVICE_DB_PLUGBOARD = False

# Set this to None if the books on the device are files that the GUI can
# access in order to add the books from the device to the library
BACKLOADING_ERROR_MESSAGE = _('Cannot get files from this device')

#: Path separator for paths to books on device path_sep = os.sep

#: Icon for this device icon = 'reader.png'

# Encapsulates an annotation fetched from the device
UserAnnotation = namedtuple('Annotation','type, value')

#: GUI displays this as a message if not None in the status bar. Useful if opening can take a
#: long time
OPEN_FEEDBACK_MESSAGE = None

#: Set of extensions that are "virtual books" on the device
#: and therefore cannot be viewed/saved/added to library.
#: For example: ``frozenset(['kobo'])``
VIRTUAL_BOOK_EXTENSIONS = frozenset()

#: Message to display to user for virtual book extensions.
VIRTUAL_BOOK_EXTENSION_MESSAGE = None

#: Whether to nuke comments in the copy of the book sent to the device. If
#: not None this should be short string that the comments will be replaced
#: by.
NUKE_COMMENTS = None

#: If True indicates that this driver completely manages device detection,
#: ejecting and so forth. If you set this to True, you *must* implement the
#: detect_managed_devices and debug_managed_device_detection methods.
#: A driver with this set to true is responsible for detection of devices,
#: managing a blacklist of devices, a list of ejected devices and so forth.
#: calibre will periodically call the detect_managed_devices() method and
#: if it returns a detected device, calibre will call open(). open() will
#: be called every time a device is returned even if previous calls to open()
#: failed, therefore the driver must maintain its own blacklist of failed
#: devices. Similarly, when ejecting, calibre will call eject() and then
#: assuming the next call to detect_managed_devices() returns None, it will
#: call post_yank_cleanup().
MANAGES_DEVICE_PRESENCE = False

#: If set the True, calibre will call the :meth:`get_driveinfo()` method
#: after the books lists have been loaded to get the driveinfo.
SLOW_DRIVEINFO = False

#: If set to True, calibre will ask the user if they want to manage the
#: device with calibre, the first time it is detected. If you set this to
#: True you must implement :meth:`get_device_uid()` and
#: :meth:`ignore_connected_device()` and
#: :meth:`get_user_blacklisted_devices` and
#: :meth:`set_user_blacklisted_devices`
ASK_TO_ALLOW_CONNECT = False

#: Set this to a dictionary of the form {'title':title, 'msg':msg, 'det_msg':detailed_msg} to have calibre popup
#: a message to the user after some callbacks are run (currently only upload_books).
#: Be careful to not spam the user with too many messages. This variable is checked after *every* callback,
#: so only set it when you really need to.
user_feedback_after_callback = None

@classmethod def get_gui_name(cls):
if hasattr(cls, 'gui_name'):
return cls.gui_name if hasattr(cls, '__name__'):
return cls.__name__ return cls.name

[문서]
@classmethod def get_open_popup_message(self):
' GUI displays this as a non-modal popup. Should be an instance of OpenPopupMessage '
return

[문서]
@classmethod def model_metadata(self) -> tuple[ModelMetadata, ...]:
' Metadata about all the actual device models this driver supports '
return ()

# Device detection {{{ def test_bcd(self, bcdDevice, bcd):
if bcd is None or len(bcd) == 0:
return True for c in bcd:
if c == bcdDevice:
return True return False

[문서]
def is_usb_connected(self, devices_on_system, debug=False, only_presence=False):
'''
Return True, device_info if a device handled by this plugin is currently connected.

:param devices_on_system: List of devices currently connected

'''
vendors_on_system = {x[0] for x in devices_on_system}
vendors = set(self.VENDOR_ID) if hasattr(self.VENDOR_ID, '__len__') else {self.VENDOR_ID}
if hasattr(self.VENDOR_ID, 'keys'):
products = []
for ven in self.VENDOR_ID:
products.extend(self.VENDOR_ID[ven].keys())
else:
products = self.PRODUCT_ID if hasattr(self.PRODUCT_ID, '__len__') else [self.PRODUCT_ID]

ch = self.can_handle_windows if iswindows else self.can_handle for vid in vendors_on_system.intersection(vendors):
for dev in devices_on_system:
cvid, pid, bcd = dev[:3]
if cvid == vid:
if pid in products:
if hasattr(self.VENDOR_ID, 'keys'):
try:
cbcd = self.VENDOR_ID[vid][pid]
except KeyError:
# Vendor vid does not have product pid, pid
# exists for some other vendor in this
# device continue else:
cbcd = self.BCD if self.test_bcd(bcd, cbcd):
if debug:
prints(dev)
if ch(dev, debug=debug):
return True, dev return False, None

[문서]
def detect_managed_devices(self, devices_on_system, force_refresh=False):
'''
Called only if MANAGES_DEVICE_PRESENCE is True.

Scan for devices that this driver can handle. Should return a device object if a device is found. This object will be passed to the open()
method as the connected_device. If no device is found, return None. The returned object can be anything, calibre does not use it, it is only passed to open().

This method is called periodically by the GUI, so make sure it is not too resource intensive. Use a cache to avoid repeatedly scanning the system.

:param devices_on_system: Set of USB devices found on the system.

:param force_refresh: If True and the driver uses a cache to prevent repeated scanning, the cache must be flushed.

'''
raise NotImplementedError()

[문서]
def debug_managed_device_detection(self, devices_on_system, output):
'''
Called only if MANAGES_DEVICE_PRESENCE is True.

Should write information about the devices detected on the system to output, which is a file like object.

Should return True if a device was detected and successfully opened, otherwise False.
'''
raise NotImplementedError()

# }}}

[문서]
def reset(self, key='-1', log_packets=False, report_progress=None, detected_device=None):
'''
:param key: The key to unlock the device
:param log_packets: If true the packet stream to/from the device is logged
:param report_progress: Function that is called with a % progress
(number between 0 and 100) for various tasks.
If it is called with -1 that means that the task does not have any progress information
:param detected_device: Device information from the device scanner

'''
raise NotImplementedError()

[문서]
def can_handle_windows(self, usbdevice, debug=False):
'''
Optional method to perform further checks on a device to see if this driver is capable of handling it. If it is not it should return False. This method is only called after the vendor, product ids and the bcd have matched, so it can do some relatively time intensive checks. The default implementation returns True. This method is called only on Windows. See also
:meth:`can_handle`.

Note that for devices based on USBMS this method by default delegates to :meth:`can_handle`. So you only need to override :meth:`can_handle` in your subclass of USBMS.

:param usbdevice: A usbdevice as returned by :func:`calibre.devices.winusb.scan_usb_devices`
'''
return True

[문서]
def can_handle(self, device_info, debug=False):
'''
Unix version of :meth:`can_handle_windows`.

:param device_info: Is a tuple of (vid, pid, bcd, manufacturer, product, serial number)

'''

return True
can_handle.is_base_class_implementation = True

[문서]
def open(self, connected_device, library_uuid):
'''
Perform any device specific initialization. Called after the device is detected but before any other functions that communicate with the device.
For example: For devices that present themselves as USB Mass storage devices, this method would be responsible for mounting the device or if the device has been automounted, for finding out where it has been mounted. The method :meth:`calibre.devices.usbms.device.Device.open` has an implementation of this function that should serve as a good example for USB Mass storage devices.

This method can raise an OpenFeedback exception to display a message to the user.

:param connected_device: The device that we are trying to open. It is a tuple of (vendor id, product id, bcd, manufacturer name, product name, device serial number). However, some devices have no serial number and on Windows only the first three fields are present, the rest are None.

:param library_uuid: The UUID of the current calibre library. Can be
None if there is no library (for example when used from the command line).

'''
raise NotImplementedError()

[문서]
def eject(self):
'''
Un-mount / eject the device from the OS. This does not check if there are pending GUI jobs that need to communicate with the device.

NOTE: That this method may not be called on the same thread as the rest of the device methods.
'''
raise NotImplementedError()

[문서]
def post_yank_cleanup(self):
'''
Called if the user yanks the device without ejecting it first.
'''
raise NotImplementedError()

[문서]
def set_progress_reporter(self, report_progress):
'''
Set a function to report progress information.

:param report_progress: Function that is called with a % progress
(number between 0 and 100) for various tasks.
If it is called with -1 that means that the task does not have any progress information

'''
raise NotImplementedError()

[문서]
def get_device_information(self, end_session=True):
'''
Ask device for device information. See L{DeviceInfoQuery}.

:return: (device name, device version, software version on device, MIME type)
The tuple can optionally have a fifth element, which is a drive information dictionary. See usbms.driver for an example.

'''
raise NotImplementedError()

[문서]
def get_driveinfo(self):
'''
Return the driveinfo dictionary. Usually called from get_device_information(), but if loading the driveinfo is slow for this driver, then it should set SLOW_DRIVEINFO. In this case, this method will be called by calibre after the book lists have been loaded. Note that it is not called on the device thread, so the driver should cache the drive info in the books() method and this function should return the cached data.
'''
return {}

[문서]
def card_prefix(self, end_session=True):
'''
Return a 2 element list of the prefix to paths on the cards.
If no card is present None is set for the card's prefix.
E.G.
('/place', '/place2')
(None, 'place2')
('place', None)
(None, None)
'''
raise NotImplementedError()

[문서]
def total_space(self, end_session=True):
'''
Get total space available on the mountpoints:
1. Main memory
2. Memory Card A
3. Memory Card B

:return: A 3 element list with total space in bytes of (1, 2, 3). If a particular device doesn't have any of these locations it should return 0.

'''
raise NotImplementedError()

[문서]
def free_space(self, end_session=True):
'''
Get free space available on the mountpoints:
1. Main memory
2. Card A
3. Card B

:return: A 3 element list with free space in bytes of (1, 2, 3). If a particular device doesn't have any of these locations it should return -1.

'''
raise NotImplementedError()

[문서]
def books(self, oncard=None, end_session=True):
'''
Return a list of e-books on the device.

:param oncard: If 'carda' or 'cardb' return a list of e-books on the specific storage card, otherwise return list of e-books in main memory of device. If a card is specified and no books are on the card return empty list.

:return: A BookList.

'''
raise NotImplementedError()

[문서]
def upload_books(self, files, names, on_card=None, end_session=True, metadata=None):
'''
Upload a list of books to the device. If a file already exists on the device, it should be replaced.
This method should raise a :class:`FreeSpaceError` if there is not enough free space on the device. The text of the FreeSpaceError must contain the word "card" if ``on_card`` is not None otherwise it must contain the word "memory".

:param files: A list of paths
:param names: A list of file names that the books should have once uploaded to the device. len(names) == len(files)
:param metadata: If not None, it is a list of :class:`Metadata` objects.
The idea is to use the metadata to determine where on the device to put the book. len(metadata) == len(files). Apart from the regular cover (path to cover), there may also be a thumbnail attribute, which should be used in preference. The thumbnail attribute is of the form
(width, height, cover_data as jpeg).

:return: A list of 3-element tuples. The list is meant to be passed to :meth:`add_books_to_metadata`.
'''
raise NotImplementedError()

[문서]
@classmethod def add_books_to_metadata(cls, locations, metadata, booklists):
'''
Add locations to the booklists. This function must not communicate with the device.

:param locations: Result of a call to L{upload_books}
:param metadata: List of :class:`Metadata` objects, same as for
:meth:`upload_books`.
:param booklists: A tuple containing the result of calls to
(:meth:`books(oncard=None)`,
:meth:`books(oncard='carda')`,
:meth`books(oncard='cardb')`).

'''
raise NotImplementedError()

[문서]
def delete_books(self, paths, end_session=True):
'''
Delete books at paths on device.
'''
raise NotImplementedError()

[문서]
@classmethod def remove_books_from_metadata(cls, paths, booklists):
'''
Remove books from the metadata list. This function must not communicate with the device.

:param paths: paths to books on the device.
:param booklists: A tuple containing the result of calls to
(:meth:`books(oncard=None)`,
:meth:`books(oncard='carda')`,
:meth`books(oncard='cardb')`).

'''
raise NotImplementedError()

[문서]
def sync_booklists(self, booklists, end_session=True):
'''
Update metadata on device.

:param booklists: A tuple containing the result of calls to
(:meth:`books(oncard=None)`,
:meth:`books(oncard='carda')`,
:meth`books(oncard='cardb')`).

'''
raise NotImplementedError()

[문서]
def get_file(self, path, outfile, end_session=True):
'''
Read the file at ``path`` on the device and write it to outfile.

:param outfile: file object like ``sys.stdout`` or the result of an
:func:`open` call.

'''
raise NotImplementedError()

[문서]
@classmethod def config_widget(cls):
'''
Should return a QWidget. The QWidget contains the settings for the device interface
'''
raise NotImplementedError()

[문서]
@classmethod def save_settings(cls, settings_widget):
'''
Should save settings to disk. Takes the widget created in
:meth:`config_widget` and saves all settings to disk.
'''
raise NotImplementedError()

[문서]
@classmethod def settings(cls):
'''
Should return an opts object. The opts object should have at least one attribute `format_map` which is an ordered list of formats for the device.
'''
raise NotImplementedError()

[문서]
def set_plugboards(self, plugboards, pb_func):
'''
provide the driver the current set of plugboards and a function to select a specific plugboard. This method is called immediately before add_books and sync_booklists.

pb_func is a callable with the following signature::
def pb_func(device_name, format, plugboards)

You give it the current device name (either the class name or
DEVICE_PLUGBOARD_NAME), the format you are interested in (a 'real'
format or 'device_db'), and the plugboards (you were given those by set_plugboards, the same place you got this method).

:return: None or a single plugboard instance.

'''
pass

[문서]
def set_driveinfo_name(self, location_code, name):
'''
Set the device name in the driveinfo file to 'name'. This setting will persist until the file is re-created or the name is changed again.

Non-disk devices should implement this method based on the location codes returned by the get_device_information() method.
'''
pass

[문서]
def prepare_addable_books(self, paths):
'''
Given a list of paths, returns another list of paths. These paths point to addable versions of the books.

If there is an error preparing a book, then instead of a path, the position in the returned list for that book should be a three tuple:
(original_path, the exception instance, traceback)
'''
return paths

[문서]
def startup(self):
'''
Called when calibre is starting the device. Do any initialization required. Note that multiple instances of the class can be instantiated, and thus __init__ can be called multiple times, but only one instance will have this method called. This method is called on the device thread, not the GUI thread.
'''
pass

[문서]
def shutdown(self):
'''
Called when calibre is shutting down, either for good or in preparation to restart. Do any cleanup required. This method is called on the device thread, not the GUI thread.
'''
pass

[문서]
def get_device_uid(self):
'''
Must return a unique id for the currently connected device (this is called immediately after a successful call to open()). You must implement this method if you set ASK_TO_ALLOW_CONNECT = True
'''
raise NotImplementedError()

[문서]
def ignore_connected_device(self, uid):
'''
Should ignore the device identified by uid (the result of a call to get_device_uid()) in the future. You must implement this method if you set ASK_TO_ALLOW_CONNECT = True. Note that this function is called immediately after open(), so if open() caches some state, the driver should reset that state.
'''
raise NotImplementedError()

[문서]
def get_user_blacklisted_devices(self):
'''
Return map of device uid to friendly name for all devices that the user has asked to be ignored.
'''
return {}

[문서]
def set_user_blacklisted_devices(self, devices):
'''
Set the list of device uids that should be ignored by this driver.
'''
pass

[문서]
def specialize_global_preferences(self, device_prefs):
'''
Implement this method if your device wants to override a particular preference. You must ensure that all call sites that want a preference that can be overridden use device_prefs['something'] instead of prefs['something']. Your method should call device_prefs.set_overrides(pref=val, pref=val, ...).
Currently used for:
metadata management (prefs['manage_device_metadata'])
'''
device_prefs.set_overrides()

[문서]
def set_library_info(self, library_name, library_uuid, field_metadata):
'''
Implement this method if you want information about the current calibre library. This method is called at startup and when the calibre library changes while connected.
'''
pass

# Dynamic control interface.
# The following methods are probably called on the GUI thread. Any driver
# that implements these methods must take pains to be thread safe, because
# the device_manager might be using the driver at the same time that one of
# these methods is called.

[문서]
def is_dynamically_controllable(self):
'''
Called by the device manager when starting plugins. If this method returns a string, then a) it supports the device manager's dynamic control interface, and b) that name is to be used when talking to the plugin.

This method can be called on the GUI thread. A driver that implements this method must be thread safe.
'''
return

[문서]
def start_plugin(self):
'''
This method is called to start the plugin. The plugin should begin to accept device connections however it does that. If the plugin is already accepting connections, then do nothing.

This method can be called on the GUI thread. A driver that implements this method must be thread safe.
'''
pass

[문서]
def stop_plugin(self):
'''
This method is called to stop the plugin. The plugin should no longer accept connections, and should cleanup behind itself. It is likely that this method should call shutdown. If the plugin is already not accepting connections, then do nothing.

This method can be called on the GUI thread. A driver that implements this method must be thread safe.
'''
pass

[문서]
def get_option(self, opt_string, default=None):
'''
Return the value of the option indicated by opt_string. This method can be called when the plugin is not started. Return None if the option does not exist.

This method can be called on the GUI thread. A driver that implements this method must be thread safe.
'''
return default

[문서]
def set_option(self, opt_string, opt_value):
'''
Set the value of the option indicated by opt_string. This method can be called when the plugin is not started.

This method can be called on the GUI thread. A driver that implements this method must be thread safe.
'''
pass

[문서]
def is_running(self):
'''
Return True if the plugin is started, otherwise false

This method can be called on the GUI thread. A driver that implements this method must be thread safe.
'''
return False

[문서]
def synchronize_with_db(self, db, book_id, book_metadata, first_call):
'''
Called during book matching when a book on the device is matched with a book in calibre's db. The method is responsible for synchronizing data from the device to calibre's db (if needed).

The method must return a two-value tuple. The first value is a set of calibre book ids changed if calibre's database was changed or None if the database was not changed. If the first value is an empty set then the metadata for the book on the device is updated with calibre's metadata and given back to the device, but no GUI refresh of that book is done.
This is useful when the calibre data is correct but must be sent to the device.

The second value is itself a 2-value tuple. The first value in the tuple specifies whether a book format should be sent to the device. The intent is to permit verifying that the book on the device is the same as the book in calibre. This value must be None if no book is to be sent, otherwise return the base file name on the device (a string like foobar.epub). Be sure to include the extension in the name. The device subsystem will construct a send_books job for all books with not- None returned values. Note: other than to later retrieve the extension, the name is ignored in cases where the device uses a template to generate the file name, which most do. The second value in the returned tuple indicated whether the format is future-dated. Return True if it is, otherwise return False. calibre will display a dialog to the user listing all future dated books.

Extremely important: this method is called on the GUI thread. It must be threadsafe with respect to the device manager's thread.

book_id: the calibre id for the book in the database.
book_metadata: the Metadata object for the book coming from the device.
first_call: True if this is the first call during a sync, False otherwise
'''
return (None, (None, False))

[문서]
class BookList(list):
'''
A list of books. Each Book object must have the fields

#. title
#. authors
#. size (file size of the book)
#. datetime (a UTC time tuple)
#. path (path on the device to the book)
#. thumbnail (can be None) thumbnail is either a str/bytes object with the image data or it should have an attribute image_path that stores an absolute (platform native) path to the image
#. tags (a list of strings, can be empty).

'''

__getslice__ = None
__setslice__ = None def __init__(self, oncard, prefix, settings):
pass

[문서]
def supports_collections(self):
''' Return True if the device supports collections for this book list. '''
raise NotImplementedError()

[문서]
def add_book(self, book, replace_metadata):
'''
Add the book to the booklist. Intent is to maintain any device-internal metadata. Return True if booklists must be sync'ed
'''
raise NotImplementedError()

[문서]
def remove_book(self, book):
'''
Remove a book from the booklist. Correct any device metadata at the same time
'''
raise NotImplementedError()

[문서]
def get_collections(self, collection_attributes):
'''
Return a dictionary of collections created from collection_attributes.
Each entry in the dictionary is of the form collection name:[list of books]

The list of books is sorted by book title, except for collections created from series, in which case series_index is used.

:param collection_attributes: A list of attributes of the Book object

'''
raise NotImplementedError()

class CurrentlyConnectedDevice:

def __init__(self):
self._device = None

@property def device(self):
return self._device

# A device driver can check if a device is currently connected to calibre using
# the following code::
# from calibre.device.interface import currently_connected_device
# if currently_connected_device.device is None:
# # no device connected
# The device attribute will be either None or the device driver object
# (DevicePlugin instance) for the currently connected device.
currently_connected_device = CurrentlyConnectedDevice()

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 시작 »
- 모듈 코드 »
- calibre.devices.usbms.cli

calibre.devices.usbms.cli의 소스 코드

__license__ = 'GPL v3'
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'

import os import shutil import time from calibre import fsync from calibre.devices.errors import PathError from calibre.utils.filenames import case_preserving_open_file class File:

def __init__(self, path):
stats = os.stat(path)
self.is_dir = os.path.isdir(path)
self.is_readonly = not os.access(path, os.W_OK)
self.ctime = stats.st_ctime self.wtime = stats.st_mtime self.size = stats.st_size if path.endswith(os.sep):
path = path[:-1]
self.path = path self.name = os.path.basename(path)

def check_transfer(infile, dest):
infile.seek(0)
dest.seek(0)
return infile.read() == dest.read()

[문서]
class CLI:

def get_file(self, path, outfile, end_session=True):
path = self.munge_path(path)
with open(path, 'rb') as src:
shutil.copyfileobj(src, outfile)

def put_file(self, infile, path, replace_file=False, end_session=True):
path = self.munge_path(path)
close = False if not hasattr(infile, 'read'):
infile, close = open(infile, 'rb'), True infile.seek(0)
if os.path.isdir(path):
path = os.path.join(path, infile.name)
if not replace_file and os.path.exists(path):
raise PathError('File already exists: ' + path)
dest, actual_path = case_preserving_open_file(path)
with dest:
try:
shutil.copyfileobj(infile, dest)
except OSError:
print('WARNING: First attempt to send file to device failed')
time.sleep(0.2)
infile.seek(0)
dest.seek(0)
dest.truncate()
shutil.copyfileobj(infile, dest)
fsync(dest)
# if not check_transfer(infile, dest): raise Exception('Transfer failed')
if close:
infile.close()
return actual_path def munge_path(self, path):
if path.startswith('/') and not (path.startswith(self._main_prefix) or
(self._card_a_prefix and path.startswith(self._card_a_prefix)) or
(self._card_b_prefix and path.startswith(self._card_b_prefix))):
path = self._main_prefix + path[1:]
elif path.startswith('carda:'):
path = path.replace('carda:', self._card_a_prefix[:-1])
elif path.startswith('cardb:'):
path = path.replace('cardb:', self._card_b_prefix[:-1])
return path def list(self, path, recurse=False, end_session=True, munge=True):
if munge:
path = self.munge_path(path)
if os.path.isfile(path):
return [(os.path.dirname(path), [File(path)])]
entries = [File(os.path.join(path, f)) for f in os.listdir(path)]
dirs = [(path, entries)]
for _file in entries:
if recurse and _file.is_dir:
dirs[len(dirs):] = self.list(_file.path, recurse=True, munge=False)
return dirs def mkdir(self, path, end_session=True):
if self.SUPPORTS_SUB_DIRS:
path = self.munge_path(path)
os.mkdir(path)

def rm(self, path, end_session=True):
path = self.munge_path(path)
self.delete_books([path])

def touch(self, path, end_session=True):
path = self.munge_path(path)
if not os.path.exists(path):
open(path, 'wb').close()
if not os.path.isdir(path):
os.utime(path, None)

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 시작 »
- 모듈 코드 »
- calibre.devices.usbms.device

calibre.devices.usbms.device의 소스 코드

__license__ = 'GPL v3'
__copyright__ = ('2009, John Schember <john at nachtimwald.com> '
'2009, Kovid Goyal <kovid@kovidgoyal.net>')
__docformat__ = 'restructuredtext en'

'''
Generic device driver. This is not a complete stand alone driver. It is intended to be subclassed with the relevant parts implemented for a particular device. This class handles device detection.
'''

import glob import os import re import subprocess import sys import time from collections import namedtuple from contextlib import suppress from itertools import repeat from calibre import prints from calibre.constants import is_debugging, isfreebsd, islinux, ismacos, iswindows from calibre.devices.errors import DeviceError from calibre.devices.interface import FAKE_DEVICE_SERIAL, DevicePlugin, ModelMetadata from calibre.devices.usbms.deviceconfig import DeviceConfig from calibre.utils.filenames import ascii_filename as sanitize if ismacos:
osx_sanitize_name_pat = re.compile(r'[.-]')

if iswindows:
usb_info_cache = {}

def eject_exe():
base = sys.extensions_location if hasattr(sys, 'new_app_layout') else os.path.dirname(sys.executable)
return os.path.join(base, 'calibre-eject.exe')

class USBDevice:

def __init__(self, dev):
self.idVendor = dev[0]
self.idProduct = dev[1]
self.bcdDevice = dev[2]
if iswindows:
# Getting this information requires communicating with the device
# we only do that in the can_handle_windows() method, if needed.
self.manufacturer = self.serial = self.product = None else:
self.manufacturer = dev[3]
self.product = dev[4]
self.serial = dev[5]

def match_serial(self, serial):
return self.serial and self.serial == serial def match_numbers(self, vid, pid, bcd):
return self.idVendor == vid and self.idProduct == pid and self.bcdDevice == bcd def match_strings(self, vid, pid, bcd, man, prod):
if not self.match_numbers(vid, pid, bcd):
return False if man == self.manufacturer and prod == self.product:
return True
# As of macOS 10.11.4 Apple started mangling the names returned via the
# IOKit registry. See
# https://www.mobileread.com/forums/showthread.php?t=273213 m = osx_sanitize_name_pat.sub('_', (self.manufacturer or ''))
p = osx_sanitize_name_pat.sub('_', (self.product or ''))
return m == man and p == prod

[문서]
class Device(DeviceConfig, DevicePlugin):
'''
This class provides logic common to all drivers for devices that export themselves as USB Mass Storage devices. Provides implementations for mounting/ejecting of USBMS devices on all platforms.
'''

VENDOR_ID = 0x0
PRODUCT_ID = 0x0
BCD = None

VENDOR_NAME = None

#: String identifying the main memory of the device in the Windows PnP id
#: strings
#: This can be None, string, list of strings or compiled regex
WINDOWS_MAIN_MEM = None

#: String identifying the first card of the device in the Windows PnP id
#: strings
#: This can be None, string, list of strings or compiled regex
WINDOWS_CARD_A_MEM = None

#: String identifying the second card of the device in the Windows PnP id
#: strings
#: This can be None, string, list of strings or compiled regex
WINDOWS_CARD_B_MEM = None

#: Used by the new driver detection to disambiguate main memory from
#: storage cards. Should be a regular expression that matches the
#: main memory mount point assigned by macOS
OSX_MAIN_MEM_VOL_PAT = None
OSX_EJECT_COMMAND = ['diskutil', 'eject']

MAIN_MEMORY_VOLUME_LABEL = ''
STORAGE_CARD_VOLUME_LABEL = ''
STORAGE_CARD2_VOLUME_LABEL = None

EBOOK_DIR_MAIN = ''
EBOOK_DIR_CARD_A = ''
EBOOK_DIR_CARD_B = ''
DELETE_EXTS = []

# USB disk-based devices can see the book files on the device, so can
# copy these back to the library
BACKLOADING_ERROR_MESSAGE = None

#: The maximum length of paths created on the device
MAX_PATH_LEN = 250

#: Put news in its own folder
NEWS_IN_FOLDER = True connected_folder_path = '' # used internally for fake folder device eject_connected_folder = False

[문서]
@classmethod def model_metadata(cls) -> tuple[ModelMetadata, ...]:
def get_representative_ids() -> tuple[int, int, int]:
vid = pid = bcd = 0 if isinstance(cls.VENDOR_ID, dict):
for vid, pid_map in cls.VENDOR_ID.items():
for pid, bcds in pid_map.items():
if isinstance(bcds, int):
bcds = (bcds,)
for bcd in bcds:
return vid or 0, pid or 0, bcd or 0 elif isinstance(cls.VENDOR_ID, (list, tuple)):
vid = cls.VENDOR_ID[-1]
else:
vid = cls.VENDOR_ID if isinstance(cls.PRODUCT_ID, (list, tuple)):
pid = cls.PRODUCT_ID[-1]
else:
pid = cls.PRODUCT_ID if isinstance(cls.BCD, (list, tuple)):
bcd = cls.BCD[-1]
else:
bcd = cls.BCD return vid or 0, pid or 0, bcd or 0 vid, pid, bcd = get_representative_ids()
try:
model_name = cls.get_gui_name()
except TypeError: # The WAYTEQ driver implements this as non classmethod return ()
parts = model_name.split(' ', 1)
manufacturer = ''
if len(parts) > 1:
manufacturer, model_name = parts else:
manufacturer = _('Miscellaneous')
return (
ModelMetadata(manufacturer, model_name, vid, pid, bcd, cls),
)

[문서]
def reset(self, key='-1', log_packets=False, report_progress=None, detected_device=None):
self._main_prefix = self._card_a_prefix = self._card_b_prefix = None self.detected_device = None if detected_device is None else USBDevice(detected_device)
self.set_progress_reporter(report_progress)

[문서]
def set_progress_reporter(self, report_progress):
self.report_progress = report_progress self.report_progress = report_progress if self.report_progress is None:
self.report_progress = lambda x, y: x

[문서]
def card_prefix(self, end_session=True):
return (self._card_a_prefix, self._card_b_prefix)

@classmethod def _windows_space(cls, prefix):
if not prefix:
return 0, 0 prefix = prefix[:-1]
from calibre_extensions import winutil try:
available_space, total_space, free_space = winutil.get_disk_free_space(prefix)
except OSError as err:
if err.winerror == winutil.ERROR_NOT_READY:
# Disk not ready time.sleep(3)
available_space, total_space, free_space = winutil.get_disk_free_space(prefix)
else:
raise return total_space, available_space

[문서]
def total_space(self, end_session=True):
msz = casz = cbsz = 0 if not iswindows:
if self._main_prefix is not None:
stats = os.statvfs(self._main_prefix)
msz = stats.f_frsize * (stats.f_blocks + stats.f_bavail - stats.f_bfree)
if self._card_a_prefix is not None:
stats = os.statvfs(self._card_a_prefix)
casz = stats.f_frsize * (stats.f_blocks + stats.f_bavail - stats.f_bfree)
if self._card_b_prefix is not None:
stats = os.statvfs(self._card_b_prefix)
cbsz = stats.f_frsize * (stats.f_blocks + stats.f_bavail - stats.f_bfree)
else:
msz = self._windows_space(self._main_prefix)[0]
casz = self._windows_space(self._card_a_prefix)[0]
cbsz = self._windows_space(self._card_b_prefix)[0]

return msz, casz, cbsz

[문서]
def free_space(self, end_session=True):
msz = casz = cbsz = 0 if not iswindows:
if self._main_prefix is not None:
stats = os.statvfs(self._main_prefix)
msz = stats.f_frsize * stats.f_bavail if self._card_a_prefix is not None:
stats = os.statvfs(self._card_a_prefix)
casz = stats.f_frsize * stats.f_bavail if self._card_b_prefix is not None:
stats = os.statvfs(self._card_b_prefix)
cbsz = stats.f_frsize * stats.f_bavail else:
msz = self._windows_space(self._main_prefix)[1]
casz = self._windows_space(self._card_a_prefix)[1]
cbsz = self._windows_space(self._card_b_prefix)[1]

return msz, casz, cbsz
def windows_filter_pnp_id(self, pnp_id):
return False

[문서]
def windows_sort_drives(self, drives):
'''
Called to disambiguate main memory and storage card for devices that do not distinguish between them on the basis of `WINDOWS_CARD_NAME`.
For example: The EB600
'''
return drives

[문서]
def can_handle_windows(self, usbdevice, debug=False):
if hasattr(self.can_handle, 'is_base_class_implementation'):
# No custom can_handle implementation return True
# Delegate to the unix can_handle function, creating a unix like
# USBDevice object from calibre.devices.winusb import get_usb_info dev = usb_info_cache.get(usbdevice)
if dev is None:
try:
data = get_usb_info(usbdevice, debug=debug)
except Exception:
time.sleep(0.1)
try:
data = get_usb_info(usbdevice, debug=debug)
except Exception:
data = {}
dev = usb_info_cache[usbdevice] = namedtuple(
'USBDevice', 'vendor_id product_id bcd manufacturer product serial')( usbdevice.vendor_id, usbdevice.product_id, usbdevice.bcd, data.get('manufacturer') or '', data.get('product') or '', data.get('serial_number') or '')
if debug:
prints(f'USB Info for device: {dev}')
return self.can_handle(dev, debug=debug)

def open_windows(self):
from calibre.devices.scanner import drive_is_ok from calibre.devices.winusb import get_drive_letters_for_device usbdev = self.device_being_opened debug = is_debugging() or getattr(self, 'do_device_debug', False)
try:
dlmap = get_drive_letters_for_device(usbdev, debug=debug)
except Exception:
dlmap = {}

if not dlmap.get('drive_letters'):
time.sleep(7)
dlmap = get_drive_letters_for_device(usbdev, debug=debug)

if debug:
from pprint import pformat prints(f'Drive letters for {usbdev}')
prints(pformat(dlmap))

filtered = set()
for dl in dlmap['drive_letters']:
pnp_id = dlmap['pnp_id_map'][dl].upper()
if dl in dlmap['readonly_drives']:
filtered.add(dl)
if debug:
prints(f'Ignoring the drive {dl} as it is readonly')
elif self.windows_filter_pnp_id(pnp_id):
filtered.add(dl)
if debug:
prints(f'Ignoring the drive {dl} because of a PNP filter on {pnp_id}')
elif not drive_is_ok(dl, debug=debug):
filtered.add(dl)
if debug:
prints(f'Ignoring the drive {dl} because failed to get free space for it')
dlmap['drive_letters'] = [dl for dl in dlmap['drive_letters'] if dl not in filtered]

if not dlmap['drive_letters']:
raise DeviceError(_('Unable to detect any disk drives for the device: %s. Try rebooting') % self.get_gui_name())

drives = {}

for drive_letter, which in zip(dlmap['drive_letters'], 'main carda cardb'.split()):
drives[which] = drive_letter + ':\\'

drives = self.windows_sort_drives(drives)
self._main_prefix = drives.get('main')
self._card_a_prefix = drives.get('carda', None)
self._card_b_prefix = drives.get('cardb', None)

@classmethod def run_ioreg(cls, raw=None):
if raw is not None:
return raw ioreg = '/usr/sbin/ioreg'
if not os.access(ioreg, os.X_OK):
ioreg = 'ioreg'
cmd = (ioreg+' -w 0 -S -c IOMedia').split()
for i in range(3):
try:
return subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
except OSError: # Probably an interrupted system call if i == 2:
raise time.sleep(2)

def osx_sort_names(self, names):
return names

@classmethod def osx_run_mount(cls):
for i in range(3):
try:
return subprocess.Popen('mount', stdout=subprocess.PIPE).communicate()[0].decode('utf-8', 'replace')
except OSError: # Probably an interrupted system call if i == 2:
raise time.sleep(2)

@classmethod def osx_get_usb_drives(cls):
from calibre_extensions.usbobserver import get_usb_drives return get_usb_drives()

def _osx_bsd_names(self):
drives = self.osx_get_usb_drives()
matches = []
d = self.detected_device if d.serial:
for path, vid, pid, bcd, ven, prod, serial in drives:
if d.match_serial(serial):
matches.append(path)
if not matches and d.manufacturer and d.product:
for path, vid, pid, bcd, man, prod, serial in drives:
if d.match_strings(vid, pid, bcd, man, prod):
matches.append(path)
if not matches:
# Since Apple started mangling the names stored in the IOKit
# registry, we cannot trust match_strings() so fallback to matching
# on just numbers. See http://www.mobileread.com/forums/showthread.php?t=273213 for path, vid, pid, bcd, man, prod, serial in drives:
if d.match_numbers(vid, pid, bcd):
matches.append(path)
if not matches:
from pprint import pformat raise DeviceError( f'Could not detect BSD names for {self.name}. Try rebooting.\nOutput from osx_get_usb_drives():\n{pformat(drives)}')

pat = re.compile(r'(?P<m>\d+)([a-z]+(?P<p>\d+)){0,1}')

def nums(x):
'Return (disk num, partition number)'
m = pat.search(x)
if m is None:
return (10000, -1)
g = m.groupdict()
if g['p'] is None:
g['p'] = 0 return list(map(int, (g.get('m'), g.get('p'))))

def cmp_key(x):
'''
Sorting based on the following scheme:
- disks without partitions are first
- sub sorted based on disk number
- disks with partitions are sorted first on disk number, then on partition number
'''
x = x.rpartition('/')[-1]
disk_num, part_num = nums(x)
has_part = 1 if part_num > 0 else 0 return has_part, disk_num, part_num matches.sort(key=cmp_key)
drives = {'main':matches[0]}
if len(matches) > 1:
drives['carda'] = matches[1]
if len(matches) > 2:
drives['cardb'] = matches[2]

return drives def osx_bsd_names(self):
drives = {}
for i in range(3):
try:
drives = self._osx_bsd_names()
if len(drives) > 1: # wait for device to settle and SD card (if any) to become available return drives except Exception:
if i == 2:
raise time.sleep(3)
return drives def open_osx(self):
from calibre_extensions.usbobserver import get_mounted_filesystems bsd_drives = self.osx_bsd_names()
drives = self.osx_sort_names(bsd_drives.copy())
mount_map = get_mounted_filesystems()
# macOS 13 Ventura uses a weird scheme for mounted FAT devices of the
# form fat://basename_of_bsd_name/basename_of_mountpoint
# see https://www.mobileread.com/forums/showthread.php?t=347294 for dev_node in tuple(mount_map):
if ':' in dev_node and '//' in dev_node:
val = mount_map[dev_node]
dev_node = dev_node.split('/')[-2]
dev_node = f'/dev/{dev_node}'
if dev_node not in mount_map:
mount_map[dev_node] = val drives = {k: mount_map.get(v) for k, v in drives.items()}
if is_debugging():
print()
from pprint import pprint pprint({'bsd_drives': bsd_drives, 'mount_map': mount_map, 'drives': drives})
if drives.get('carda') is None and drives.get('cardb') is not None:
drives['carda'] = drives.pop('cardb')
if drives.get('main') is None and drives.get('carda') is not None:
drives['main'] = drives.pop('carda')
if drives.get('carda') is None and drives.get('cardb') is not None:
drives['carda'] = drives.pop('cardb')
if drives.get('main') is None:
raise DeviceError(_('Unable to detect the %s mount point. Try rebooting.')%self.__class__.__name__)
pat = self.OSX_MAIN_MEM_VOL_PAT if pat is not None and len(drives) > 1 and 'main' in drives:
if pat.search(drives['main']) is None:
main = drives['main']
for x in ('carda', 'cardb'):
if x in drives and pat.search(drives[x]):
drives['main'] = drives.pop(x)
drives[x] = main break self._main_prefix = drives['main']+os.sep def get_card_prefix(c):
ans = drives.get(c, None)
if ans is not None:
ans += os.sep return ans self._card_a_prefix = get_card_prefix('carda')
self._card_b_prefix = get_card_prefix('cardb')

def find_device_nodes(self, detected_device=None):

def walk(base):
base = os.path.abspath(os.path.realpath(base))
for x in os.listdir(base):
p = os.path.join(base, x)
if os.path.islink(p) or not os.access(p, os.R_OK):
continue isfile = os.path.isfile(p)
yield p, isfile if not isfile:
yield from walk(p)

def raw2num(raw):
raw = raw.lower()
if not raw.startswith('0x'):
raw = '0x' + raw return int(raw, 16)

# Find device node based on vendor, product and bcd d, j = os.path.dirname, os.path.join usb_dir = None if detected_device is None:
detected_device = self.detected_device def test(val, attr):
q = getattr(detected_device, attr)
return q == val def getnum(usb_dir):
def rc(q):
with open(j(usb_dir, q), 'rb') as f:
return raw2num(f.read().decode('utf-8'))
return rc for x, isfile in walk('/sys/devices'):
if isfile and x.endswith('idVendor'):
usb_dir = d(x)
for y in ('idProduct', 'idVendor', 'bcdDevice'):
if not os.access(j(usb_dir, y), os.R_OK):
usb_dir = None break if usb_dir is None:
continue ven, prod, bcd = map(getnum(usb_dir), ('idVendor', 'idProduct', 'bcdDevice'))
if not (test(ven, 'idVendor') and test(prod, 'idProduct') and test(bcd, 'bcdDevice')):
usb_dir = None continue else:
break if usb_dir is None:
raise DeviceError(_('Unable to detect the %s disk drive.')
%self.__class__.__name__)

devnodes, ok = [], {}
for x, isfile in walk(usb_dir):
if not isfile and '/block/' in x:
parts = x.split('/')
idx = parts.index('block')
if idx == len(parts)-2:
sz = j(x, 'size')
node = parts[idx+1]
try:
with open(sz, 'rb') as szf:
exists = int(szf.read().decode('utf-8')) > 0 if exists:
node = self.find_largest_partition(x)
ok[node] = True else:
ok[node] = False except Exception:
ok[node] = False if is_debugging() and not ok[node]:
print(f'\nIgnoring the node: {node} as could not read size from: {sz}')

devnodes.append(node)

devnodes += list(repeat(None, 3))
ans = ['/dev/'+x if ok.get(x) else None for x in devnodes]
ans.sort(key=lambda x: x[5:] if x else 'zzzzz')
return self.linux_swap_drives(ans[:3])

def linux_swap_drives(self, drives):
return drives def node_mountpoint(self, node):
from calibre.devices.udisks import node_mountpoint return node_mountpoint(node)

def find_largest_partition(self, path):
node = path.split('/')[-1]
nodes = []
for x in glob.glob(path+'/'+node+'*'):
sz = x + '/size'

if not os.access(sz, os.R_OK):
continue try:
with open(sz, 'rb') as szf:
sz = int(szf.read().decode('utf-8'))
except Exception:
continue if sz > 0:
nodes.append((x.split('/')[-1], sz))

nodes.sort(key=lambda x: x[1])
if not nodes:
return node return nodes[-1][0]

def open_linux(self):

def mount(node, type):
mp = self.node_mountpoint(node)
if mp is not None:
return mp, 0 def do_mount(node):
try:
from calibre.devices.udisks import mount mount(node)
return 0 except Exception:
print('Udisks mount call failed:')
import traceback traceback.print_exc()
return 1 ret = do_mount(node)
if ret != 0:
return None, ret return self.node_mountpoint(node)+'/', 0 main, carda, cardb = self.find_device_nodes()
if main is None:
raise DeviceError(_('Unable to detect the %s disk drive. Either '
'the device has already been ejected, or your '
'kernel is exporting a deprecated version of SYSFS.')
%self.__class__.__name__)
if is_debugging():
print('\nFound device nodes:', main, carda, cardb)

self._linux_mount_map = {}
mp, ret = mount(main, 'main')
if mp is None:
raise DeviceError(
_('Unable to mount main memory (Error code: %d)')%ret)
if not mp.endswith('/'):
mp += '/'
self._linux_mount_map[main] = mp self._main_prefix = mp self._linux_main_device_node = main cards = [(carda, '_card_a_prefix', 'carda'),
(cardb, '_card_b_prefix', 'cardb')]
for card, prefix, typ in cards:
if card is None:
continue mp, ret = mount(card, typ)
if mp is None:
print(f'Unable to mount card (Error code: {ret})', file=sys.stderr)
else:
if not mp.endswith('/'):
mp += '/'
setattr(self, prefix, mp)
self._linux_mount_map[card] = mp self.filter_read_only_mount_points()

def filter_read_only_mount_points(self):

def is_readonly(mp):
if mp is None:
return True path = os.path.join(mp, 'calibre_readonly_test')
ro = True try:
with open(path, 'wb'):
ro = False except Exception:
pass else:
try:
os.remove(path)
except Exception:
pass if is_debugging() and ro:
print('\nThe mountpoint', mp, 'is readonly, ignoring it')
return ro for mp in ('_main_prefix', '_card_a_prefix', '_card_b_prefix'):
if is_readonly(getattr(self, mp, None)):
setattr(self, mp, None)

if self._main_prefix is None:
for p in ('_card_a_prefix', '_card_b_prefix'):
nmp = getattr(self, p, None)
if nmp is not None:
self._main_prefix = nmp setattr(self, p, None)
break if self._main_prefix is None:
raise DeviceError(_('The main memory of %s is read only. '
'This usually happens because of file system errors.')
%self.__class__.__name__)

if self._card_a_prefix is None and self._card_b_prefix is not None:
self._card_a_prefix = self._card_b_prefix self._card_b_prefix = None

# ------------------------------------------------------
#
# open for FreeBSD
# find the device node or nodes that match the S/N we already have from the scanner
# and attempt to mount each one
# 1. get list of devices via DBUS UDisk2 with matching s/n etc.
# 2. get list of volumes associated with each
# 3. attempt to mount each one using UDisks2
# 4. when finished, we have a list of mount points and associated dbus nodes
# def open_freebsd(self):
from calibre.devices.udisks import find_device_vols_by_serial

# There should be some way to access the -v arg...
verbose = False

# this gives us access to the S/N, etc. of the reader that the scanner has found
# and the match routines for some of that data, like s/n, vendor ID, etc.
d=self.detected_device if not d.serial:
raise DeviceError("Device has no S/N. Can't continue")

vols = find_device_vols_by_serial(d.serial)

if verbose:
print('FBSD:\t', vols)

ok, mv = self.freebsd_mount_volumes(vols)
if not ok:
raise DeviceError(_('Unable to mount the device'))
for k, v in mv.items():
setattr(self, k, v)

def freebsd_mount_volumes(self, vols):
def fmount(node):
mp = self.node_mountpoint(node)
if mp is not None:
# Already mounted return mp from calibre.devices.udisks import mount, rescan for i in range(6):
try:
mp = mount(node)
break except Exception:
if i < 5:
rescan(node)
time.sleep(1)
else:
print('Udisks mount call failed:')
import traceback traceback.print_exc()

return mp mp = None mtd = 0 ans = {
'_main_prefix': None, '_main_vol': None,
'_card_a_prefix': None, '_card_a_vol': None,
'_card_b_prefix': None, '_card_b_vol': None,
}
for vol in vols:
try:
mp = fmount(vol['Device'])
except Exception:
print('Failed to mount: ' + vol['Device'])
import traceback traceback.print_exc()

if mp is None:
continue

# Mount Point becomes Mount Path mp += '/'
DEBUG = is_debugging()
if DEBUG:
print('FBSD:\tmounted', vol['Device'], 'on', mp)
if mtd == 0:
ans['_main_prefix'], ans['_main_vol'] = mp, vol['Device']
if DEBUG:
print('FBSD:\tmain = ', mp)
elif mtd == 1:
ans['_card_a_prefix'], ans['_card_a_vol'] = mp, vol['Device']
if DEBUG:
print('FBSD:\tcard a = ', mp)
elif mtd == 2:
ans['_card_b_prefix'], ans['_card_b_vol'] = mp, vol['Device']
if DEBUG:
print('FBSD:\tcard b = ', mp)
break mtd += 1 return mtd > 0, ans

#
# ------------------------------------------------------
#
# this one is pretty simple:
# just umount each of the previously
# mounted filesystems, using the stored volume object
# def eject_freebsd(self):
from calibre.devices.udisks import umount if self._main_prefix:
umount(self._main_vol)
if self._card_a_prefix:
umount(self._card_a_vol)
if self._card_b_prefix:
umount(self._card_b_vol)

self._main_prefix = self._main_vol = None self._card_a_prefix = self._card_a_vol = None self._card_b_prefix = self._card_b_vol = None
# ------------------------------------------------------ def is_folder_still_available(self):
if self.eject_connected_folder:
self.eject_connected_folder = False self.connected_folder_path = ''
with suppress(OSError):
if self.connected_folder_path:
return os.path.isdir(self.connected_folder_path)
return False

[문서]
def open(self, connected_device, library_uuid):
self._main_prefix = self._card_a_prefix = self._card_b_prefix = None self.connected_folder_path = ''
if getattr(connected_device, 'serial', None) and connected_device.serial.startswith(FAKE_DEVICE_SERIAL):
folder_path = connected_device.serial[len(FAKE_DEVICE_SERIAL):]
if not os.path.isdir(folder_path):
raise DeviceError(f'The path {folder_path} is not a folder cannot connect to it')
if not os.access(folder_path, os.R_OK | os.W_OK):
raise DeviceError(f'You do not have permission to read and write to {folder_path} cannot connect to it')
if not folder_path.endswith(os.sep) and not folder_path.endswith('/'):
folder_path += os.sep self._main_prefix = folder_path self.current_library_uuid = library_uuid self.device_being_opened = connected_device try:
self.post_open_callback()
finally:
self.device_being_opened = None self.connected_folder_path = folder_path return time.sleep(5)
self.device_being_opened = connected_device try:
if islinux:
try:
self.open_linux()
except DeviceError:
time.sleep(7)
self.open_linux()
if isfreebsd:
self._main_vol = self._card_a_vol = self._card_b_vol = None self.open_freebsd()
if iswindows:
self.open_windows()
if ismacos:
try:
self.open_osx()
except DeviceError:
time.sleep(7)
self.open_osx()

self.current_library_uuid = library_uuid self.post_open_callback()
finally:
self.device_being_opened = Nonedef post_open_callback(self):
pass def eject_windows(self):
from threading import Thread drives = []
for x in ('_main_prefix', '_card_a_prefix', '_card_b_prefix'):
x = getattr(self, x, None)
if x is not None:
drives.append(x[0].upper())

def do_it(drives):
subprocess.Popen([eject_exe()] + drives, creationflags=subprocess.CREATE_NO_WINDOW).wait()

t = Thread(target=do_it, args=[drives])
t.daemon = True t.start()
self.__save_win_eject_thread = t def eject_osx(self):
for x in ('_main_prefix', '_card_a_prefix', '_card_b_prefix'):
x = getattr(self, x, None)
if x is not None:
try:
subprocess.Popen(self.OSX_EJECT_COMMAND + [x])
except Exception:
pass def eject_linux(self):
from calibre.devices.udisks import eject, umount drives = [d for d in self.find_device_nodes() if d]
for d in drives:
try:
umount(d)
except Exception:
pass for d in drives:
try:
eject(d)
except Exception as e:
print('Udisks eject call for:', d, 'failed:')
print('\t', e)

def on_device_close(self):
pass def unmount_device(self):
if self.connected_folder_path:
self.eject_connected_folder = True

[문서]
def eject(self):
if islinux:
try:
self.eject_linux()
except Exception:
pass if isfreebsd:
try:
self.eject_freebsd()
except Exception:
pass if iswindows:
try:
self.eject_windows()
except Exception:
pass if ismacos:
try:
self.eject_osx()
except Exception:
pass self._main_prefix = self._card_a_prefix = self._card_b_prefix = None self.on_device_close()

def linux_post_yank(self):
self._linux_mount_map = {}

[문서]
def post_yank_cleanup(self):
if islinux:
try:
self.linux_post_yank()
except Exception:
import traceback traceback.print_exc()
self._main_prefix = self._card_a_prefix = self._card_b_prefix = None self.on_device_close()

def get_main_ebook_dir(self, for_upload=False):
return self.EBOOK_DIR_MAIN def get_carda_ebook_dir(self, for_upload=False):
return self.EBOOK_DIR_CARD_A def get_cardb_ebook_dir(self, for_upload=False):
return self.EBOOK_DIR_CARD_B def _sanity_check(self, on_card, files):
from calibre.devices.utils import sanity_check sanity_check(on_card, files, self.card_prefix(), self.free_space())

def get_dest_dir(prefix, candidates):
if isinstance(candidates, (str, bytes)):
candidates = [candidates]
if not candidates:
candidates = ['']
candidates = [
((os.path.join(prefix, *(x.split('/')))) if x else prefix)
for x in candidates]
existing = [x for x in candidates if os.path.exists(x)]
if not existing:
existing = candidates return existing[0]

if on_card == 'carda':
candidates = self.get_carda_ebook_dir(for_upload=True)
path = get_dest_dir(self._card_a_prefix, candidates)
elif on_card == 'cardb':
candidates = self.get_cardb_ebook_dir(for_upload=True)
path = get_dest_dir(self._card_b_prefix, candidates)
else:
candidates = self.get_main_ebook_dir(for_upload=True)
path = get_dest_dir(self._main_prefix, candidates)

return path

[문서]
def sanitize_callback(self, path):
'''
Callback to allow individual device drivers to override the path sanitization used by :meth:`create_upload_path`.
'''
return sanitize(path)

[문서]
def filename_callback(self, default, mi):
'''
Callback to allow drivers to change the default file name set by :meth:`create_upload_path`.
'''
return default

[문서]
def sanitize_path_components(self, components):
'''
Perform any device specific sanitization on the path components for files to be uploaded to the device
'''
return components

[문서]
def get_annotations(self, path_map):
'''
Resolve path_map to annotation_map of files found on the device
'''
return {}

[문서]
def add_annotation_to_library(self, db, db_id, annotation):
'''
Add an annotation to the calibre library
'''
pass
def create_upload_path(self, path, mdata, fname, create_dirs=True):
from calibre.devices.utils import create_upload_path settings = self.settings()
filepath = create_upload_path(mdata, fname, self.save_template(), self.sanitize_callback, prefix_path=os.path.abspath(path), maxlen=self.MAX_PATH_LEN, use_subdirs=self.SUPPORTS_SUB_DIRS and settings.use_subdirs, news_in_folder=self.NEWS_IN_FOLDER, filename_callback=self.filename_callback, sanitize_path_components=self.sanitize_path_components
)
filedir = os.path.dirname(filepath)

if create_dirs and not os.path.exists(filedir):
os.makedirs(filedir)

return filepath def create_annotations_path(self, mdata, device_path=None):
return self.create_upload_path(os.path.abspath('/<storage>'), mdata, 'x.bookmark', create_dirs=False)

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 시작 »
- 모듈 코드 »
- calibre.devices.usbms.driver

calibre.devices.usbms.driver의 소스 코드

__license__ = 'GPL v3'
__copyright__ = '2009, John Schember <john at nachtimwald.com>'
__docformat__ = 'restructuredtext en'

'''
Generic USB Mass storage device driver. This is not a complete stand alone driver. It is intended to be subclassed with the relevant parts implemented for a particular device.
'''

import json import os import shutil from itertools import cycle from calibre import fsync, isbytestring, prints from calibre.constants import filesystem_encoding, ismacos, numeric_version from calibre.devices.usbms.books import Book, BookList from calibre.devices.usbms.cli import CLI from calibre.devices.usbms.device import Device from calibre.ebooks.metadata.book.json_codec import JsonCodec from calibre.prints import debug_print def safe_walk(top, topdown=True, onerror=None, followlinks=False, maxdepth=128):
' A replacement for os.walk that does not die when it encounters undecodeable filenames in a linux filesystem'
if maxdepth < 0:
return islink, join, isdir = os.path.islink, os.path.join, os.path.isdir

# We may not have read permission for top, in which case we can't
# get a list of the files the directory contains. os.path.walk
# always suppressed the exception then, rather than blow up for a
# minor reason when (say) a thousand readable directories are still
# left to visit. That logic is copied here.
try:
names = os.listdir(top)
except OSError as err:
if onerror is not None:
onerror(err)
return dirs, nondirs = [], []
for name in names:
if isinstance(name, bytes):
try:
name = name.decode(filesystem_encoding)
except UnicodeDecodeError:
debug_print(f'Skipping undecodeable file: {name!r}')
continue if isdir(join(top, name)):
dirs.append(name)
else:
nondirs.append(name)

if topdown:
yield top, dirs, nondirs for name in dirs:
new_path = join(top, name)
if followlinks or not islink(new_path):
yield from safe_walk(new_path, topdown, onerror, followlinks, maxdepth-1)
if not topdown:
yield top, dirs, nondirs

# CLI must come before Device as it implements the CLI functions that
# are inherited from the device interface in Device.
[문서]
class USBMS(CLI, Device):
'''
The base class for all USBMS devices. Implements the logic for sending/getting/updating metadata/caching metadata/etc.
'''

description = _('Communicate with an e-book reader.')
author = 'John Schember'
supported_platforms = ['windows', 'osx', 'linux']

# Store type instances of BookList and Book. We must do this because
# a) we need to override these classes in some device drivers, and
# b) the classmethods seem only to see real attributes declared in the
# class, not attributes stored in the class booklist_class = BookList book_class = Book

FORMATS = []
CAN_SET_METADATA = []
METADATA_CACHE = 'metadata.calibre'
DRIVEINFO = 'driveinfo.calibre'

SCAN_FROM_ROOT = False def _update_driveinfo_record(self, dinfo, prefix, location_code, name=None):
import uuid from calibre.utils.date import isoformat, now if not isinstance(dinfo, dict):
dinfo = {}
if dinfo.get('device_store_uuid', None) is None:
dinfo['device_store_uuid'] = str(uuid.uuid4())
if dinfo.get('device_name', None) is None:
dinfo['device_name'] = self.get_gui_name()
if name is not None:
dinfo['device_name'] = name dinfo['location_code'] = location_code dinfo['last_library_uuid'] = getattr(self, 'current_library_uuid', None)
dinfo['calibre_version'] = '.'.join([str(i) for i in numeric_version])
dinfo['date_last_connected'] = isoformat(now())
dinfo['prefix'] = prefix.replace('\\', '/')
return dinfo def _update_driveinfo_file(self, prefix, location_code, name=None):
from calibre.utils.config import from_json, to_json if os.path.exists(os.path.join(prefix, self.DRIVEINFO)):
with open(os.path.join(prefix, self.DRIVEINFO), 'rb') as f:
try:
driveinfo = json.loads(f.read(), object_hook=from_json)
except Exception:
driveinfo = None driveinfo = self._update_driveinfo_record(driveinfo, prefix, location_code, name)
data = json.dumps(driveinfo, default=to_json)
if not isinstance(data, bytes):
data = data.encode('utf-8')
with open(os.path.join(prefix, self.DRIVEINFO), 'wb') as f:
f.write(data)
fsync(f)
else:
driveinfo = self._update_driveinfo_record({}, prefix, location_code, name)
data = json.dumps(driveinfo, default=to_json)
if not isinstance(data, bytes):
data = data.encode('utf-8')
with open(os.path.join(prefix, self.DRIVEINFO), 'wb') as f:
f.write(data)
fsync(f)
return driveinfo

[문서]
def get_device_information(self, end_session=True):
self.report_progress(1.0, _('Get device information...'))
self.driveinfo = {}

def raise_os_error(e):
raise OSError(_('Failed to access files in the main memory of'
' your device. You should contact the device'
' manufacturer for support. Common fixes are:'
' try a different USB cable/USB port on your computer.'
' If you device has a "Reset to factory defaults" type'
' of setting somewhere, use it. Underlying error: %s')
% e) from e if self._main_prefix is not None:
try:
self.driveinfo['main'] = self._update_driveinfo_file(self._main_prefix, 'main')
except PermissionError as e:
if ismacos:
raise PermissionError(_(
'Permission was denied by macOS trying to access files in the main memory of'
' your device. You will need to grant permission explicitly by looking under'
' System Preferences > Security and Privacy > Privacy > Files and Folders.'
' Underlying error: %s'
) % e) from e raise_os_error(e)
except OSError as e:
raise_os_error(e)
try:
if self._card_a_prefix is not None:
self.driveinfo['A'] = self._update_driveinfo_file(self._card_a_prefix, 'A')
if self._card_b_prefix is not None:
self.driveinfo['B'] = self._update_driveinfo_file(self._card_b_prefix, 'B')
except OSError as e:
raise OSError(_('Failed to access files on the SD card in your'
' device. This can happen for many reasons. The SD card may be'
' corrupted, it may be too large for your device, it may be'
' write-protected, etc. Try a different SD card, or reformat'
' your SD card using the FAT32 filesystem. Also make sure'
' there are not too many files in the root of your SD card.'
' Underlying error: %s') % e)
return (self.get_gui_name(), '', '', '', self.driveinfo)

[문서]
def set_driveinfo_name(self, location_code, name):
if location_code == 'main':
self._update_driveinfo_file(self._main_prefix, location_code, name)
elif location_code == 'A':
self._update_driveinfo_file(self._card_a_prefix, location_code, name)
elif location_code == 'B':
self._update_driveinfo_file(self._card_b_prefix, location_code, name)

def formats_to_scan_for(self):
return set(self.settings().format_map) | set(self.FORMATS)

def is_allowed_book_file(self, filename, path, prefix):
return True

[문서]
def books(self, oncard=None, end_session=True):
from calibre.ebooks.metadata.meta import path_to_ext debug_print('USBMS: Fetching list of books from device. Device=', self.__class__.__name__,
'oncard=', oncard)

dummy_bl = self.booklist_class(None, None, None)

if oncard == 'carda' and not self._card_a_prefix:
self.report_progress(1.0, _('Getting list of books on device...'))
return dummy_bl elif oncard == 'cardb' and not self._card_b_prefix:
self.report_progress(1.0, _('Getting list of books on device...'))
return dummy_bl elif oncard and oncard not in {'carda', 'cardb'}:
self.report_progress(1.0, _('Getting list of books on device...'))
return dummy_bl prefix = self._card_a_prefix if oncard == 'carda' else \ self._card_b_prefix if oncard == 'cardb' \ else self._main_prefix ebook_dirs = self.get_carda_ebook_dir() if oncard == 'carda' else \ self.EBOOK_DIR_CARD_B if oncard == 'cardb' else \ self.get_main_ebook_dir()

debug_print('USBMS: dirs are:', prefix, ebook_dirs)

# get the metadata cache bl = self.booklist_class(oncard, prefix, self.settings)
need_sync = self.parse_metadata_cache(bl, prefix, self.METADATA_CACHE)

# make a dict cache of paths so the lookup in the loop below is faster.
bl_cache = {}
for idx, b in enumerate(bl):
bl_cache[b.lpath] = idx all_formats = self.formats_to_scan_for()

def update_booklist(filename, path, prefix):
changed = False
# Ignore AppleDouble files if filename.startswith('._'):
return False if path_to_ext(filename) in all_formats and self.is_allowed_book_file(filename, path, prefix):
try:
lpath = os.path.join(path, filename).partition(self.normalize_path(prefix))[2]
lpath = lpath.removeprefix(os.sep)
lpath = lpath.replace('\\', '/')
idx = bl_cache.get(lpath, None)
if idx is not None:
bl_cache[lpath] = None if self.update_metadata_item(bl[idx]):
# print('update_metadata_item returned true')
changed = True elif bl.add_book(self.book_from_path(prefix, lpath), replace_metadata=False):
changed = True except Exception: # Probably a filename encoding error import traceback traceback.print_exc()
return changed if isinstance(ebook_dirs, (str, bytes)):
ebook_dirs = [ebook_dirs]
for ebook_dir in ebook_dirs:
ebook_dir = self.path_to_unicode(ebook_dir)
if self.SCAN_FROM_ROOT:
ebook_dir = self.normalize_path(prefix)
else:
ebook_dir = self.normalize_path( os.path.join(prefix, *(ebook_dir.split('/')))
if ebook_dir else prefix)
debug_print('USBMS: scan from root', self.SCAN_FROM_ROOT, ebook_dir)
if not os.path.exists(ebook_dir):
continue
# Get all books in the ebook_dir directory if self.SUPPORTS_SUB_DIRS or self.SUPPORTS_SUB_DIRS_FOR_SCAN:
# build a list of files to check, so we can accurately report progress flist = []
for path, dirs, files in safe_walk(ebook_dir):
for filename in files:
if filename != self.METADATA_CACHE:
flist.append({'filename': self.path_to_unicode(filename),
'path':self.path_to_unicode(path)})
for i, f in enumerate(flist):
self.report_progress(i/float(len(flist)), _('Getting list of books on device...'))
changed = update_booklist(f['filename'], f['path'], prefix)
if changed:
need_sync = True else:
paths = os.listdir(ebook_dir)
for i, filename in enumerate(paths):
self.report_progress((i+1) / float(len(paths)), _('Getting list of books on device...'))
changed = update_booklist(self.path_to_unicode(filename), ebook_dir, prefix)
if changed:
need_sync = True

# Remove books that are no longer in the filesystem. Cache contains
# indices into the booklist if book not in filesystem, None otherwise
# Do the operation in reverse order so indices remain valid for idx in sorted(bl_cache.values(), reverse=True, key=lambda x: -1 if x is None else x):
if idx is not None:
need_sync = True del bl[idx]

debug_print(f'USBMS: count found in cache: {len(bl_cache)}, count of files in metadata: {len(bl)}, need_sync: {need_sync}')
if need_sync: # self.count_found_in_bl != len(bl) or need_sync:
if oncard == 'cardb':
self.sync_booklists((None, None, bl))
elif oncard == 'carda':
self.sync_booklists((None, bl, None))
else:
self.sync_booklists((bl, None, None))

self.report_progress(1.0, _('Getting list of books on device...'))
debug_print('USBMS: Finished fetching list of books from device. oncard=', oncard)
return bl[문서]
def upload_books(self, files, names, on_card=None, end_session=True, metadata=None):
debug_print(f'USBMS: uploading {len(files)} books')

path = self._sanity_check(on_card, files)

paths = []
names = iter(names)
metadata = iter(metadata)

for i, infile in enumerate(files):
mdata, fname = next(metadata), next(names)
filepath = self.normalize_path(self.create_upload_path(path, mdata, fname))
if not hasattr(infile, 'read'):
infile = self.normalize_path(infile)
filepath = self.put_file(infile, filepath, replace_file=True)
paths.append(filepath)
try:
self.upload_cover(os.path.dirname(filepath), os.path.splitext(os.path.basename(filepath))[0], mdata, filepath)
except Exception: # Failure to upload cover is not catastrophic import traceback traceback.print_exc()

self.report_progress((i+1) / float(len(files)), _('Transferring books to device...'))

self.report_progress(1.0, _('Transferring books to device...'))
debug_print(f'USBMS: finished uploading {len(files)} books')
return list(zip(paths, cycle([on_card])))

[문서]
def upload_cover(self, path, filename, metadata, filepath):
'''
Upload book cover to the device. Default implementation does nothing.

:param path: The full path to the folder where the associated book is located.
:param filename: The name of the book file without the extension.
:param metadata: metadata belonging to the book. Use metadata.thumbnail for cover
:param filepath: The full path to the e-book file

'''
pass

[문서]
def add_books_to_metadata(self, locations, metadata, booklists):
debug_print(f'USBMS: adding metadata for {len(metadata)} books')

metadata = iter(metadata)
locations = tuple(locations)
for i, location in enumerate(locations):
self.report_progress((i+1) / float(len(locations)), _('Adding books to device metadata listing...'))
info = next(metadata)
blist = 2 if location[1] == 'cardb' else 1 if location[1] == 'carda' else 0

# Extract the correct prefix from the pathname. To do this correctly,
# we must ensure that both the prefix and the path are normalized
# so that the comparison will work. Book's __init__ will fix up
# lpath, so we don't need to worry about that here.
path = self.normalize_path(location[0])
if self._main_prefix:
prefix = self._main_prefix if \ path.startswith(self.normalize_path(self._main_prefix)) else None if not prefix and self._card_a_prefix:
prefix = self._card_a_prefix if \ path.startswith(self.normalize_path(self._card_a_prefix)) else None if not prefix and self._card_b_prefix:
prefix = self._card_b_prefix if \ path.startswith(self.normalize_path(self._card_b_prefix)) else None if prefix is None:
prints('in add_books_to_metadata. Prefix is None!', path, self._main_prefix)
continue lpath = path.partition(prefix)[2]
if lpath.startswith(('/', '\\')):
lpath = lpath[1:]
book = self.book_class(prefix, lpath, other=info)
if book.size is None:
book.size = os.stat(self.normalize_path(path)).st_size b = booklists[blist].add_book(book, replace_metadata=True)
if b:
b._new_book = True self.report_progress(1.0, _('Adding books to device metadata listing...'))
debug_print('USBMS: finished adding metadata')def delete_single_book(self, path):
os.unlink(path)

def delete_extra_book_files(self, path):
filepath = os.path.splitext(path)[0]
for ext in self.DELETE_EXTS:
for x in (filepath, path):
x += ext if os.path.exists(x):
if os.path.isdir(x):
shutil.rmtree(x, ignore_errors=True)
else:
os.unlink(x)

if self.SUPPORTS_SUB_DIRS:
try:
os.removedirs(os.path.dirname(path))
except Exception:
pass

[문서]
def delete_books(self, paths, end_session=True):
debug_print(f'USBMS: deleting {len(paths)} books')
for i, path in enumerate(paths):
self.report_progress((i+1) / float(len(paths)), _('Removing books from device...'))
path = self.normalize_path(path)
if os.path.exists(path):
# Delete the ebook self.delete_single_book(path)
self.delete_extra_book_files(path)

self.report_progress(1.0, _('Removing books from device...'))
debug_print(f'USBMS: finished deleting {len(paths)} books')

[문서]
def remove_books_from_metadata(self, paths, booklists):
debug_print(f'USBMS: removing metadata for {len(paths)} books')

for i, path in enumerate(paths):
self.report_progress((i+1) / float(len(paths)), _('Removing books from device metadata listing...'))
for bl in booklists:
for book in bl:
if path.endswith(book.path):
bl.remove_book(book)
self.report_progress(1.0, _('Removing books from device metadata listing...'))
debug_print(f'USBMS: finished removing metadata for {len(paths)} books')

# If you override this method and you use book._new_book, then you must
# complete the processing before you call this method. The flag is cleared
# at the end just before the return
[문서]
def sync_booklists(self, booklists, end_session=True):
debug_print('USBMS: starting sync_booklists')
json_codec = JsonCodec()

if not os.path.exists(self.normalize_path(self._main_prefix)):
os.makedirs(self.normalize_path(self._main_prefix))

def write_prefix(prefix, listid):
if (prefix is not None and len(booklists) > listid and isinstance(booklists[listid], self.booklist_class)):
if not os.path.exists(prefix):
os.makedirs(self.normalize_path(prefix))
with open(self.normalize_path(os.path.join(prefix, self.METADATA_CACHE)), 'wb') as f:
json_codec.encode_to_file(f, booklists[listid])
fsync(f)
write_prefix(self._main_prefix, 0)
write_prefix(self._card_a_prefix, 1)
write_prefix(self._card_b_prefix, 2)

# Clear the _new_book indication, as we are supposed to be done with
# adding books at this point for blist in booklists:
if blist is not None:
for book in blist:
book._new_book = False self.report_progress(1.0, _('Sending metadata to device...'))
debug_print('USBMS: finished sync_booklists')@classmethod def build_template_regexp(cls):
from calibre.devices.utils import build_template_regexp return build_template_regexp(cls.save_template())

@classmethod def path_to_unicode(cls, path):
if isbytestring(path):
path = path.decode(filesystem_encoding)
return path

[문서]
@classmethod def normalize_path(cls, path):
'Return path with platform native path separators'
if path is None:
return None if os.sep == '\\':
path = path.replace('/', '\\')
else:
path = path.replace('\\', '/')
return cls.path_to_unicode(path)

@classmethod def parse_metadata_cache(cls, bl, prefix, name):
json_codec = JsonCodec()
need_sync = False cache_file = cls.normalize_path(os.path.join(prefix, name))
if os.access(cache_file, os.R_OK):
try:
with open(cache_file, 'rb') as f:
json_codec.decode_from_file(f, bl, cls.book_class, prefix)
except Exception:
import traceback traceback.print_exc()
bl = []
need_sync = True else:
need_sync = True return need_sync

@classmethod def update_metadata_item(cls, book):
changed = False size = os.stat(cls.normalize_path(book.path)).st_size if size != book.size:
changed = True mi = cls.metadata_from_path(book.path)
book.smart_update(mi)
book.size = size return changed

@classmethod def metadata_from_path(cls, path):
return cls.metadata_from_formats([path])

@classmethod def metadata_from_formats(cls, fmts):
from calibre.customize.ui import quick_metadata from calibre.ebooks.metadata.meta import metadata_from_formats with quick_metadata:
return metadata_from_formats(fmts, force_read_metadata=True, pattern=cls.build_template_regexp())

@classmethod def book_from_path(cls, prefix, lpath):
from calibre.ebooks.metadata.book.base import Metadata if cls.settings().read_metadata or cls.MUST_READ_METADATA:
mi = cls.metadata_from_path(cls.normalize_path(os.path.join(prefix, lpath)))
else:
from calibre.ebooks.metadata.meta import metadata_from_filename mi = metadata_from_filename(cls.normalize_path(os.path.basename(lpath)), cls.build_template_regexp())
if mi is None:
mi = Metadata(os.path.splitext(os.path.basename(lpath))[0],
[_('Unknown')])
size = os.stat(cls.normalize_path(os.path.join(prefix, lpath))).st_size book = cls.book_class(prefix, lpath, other=mi, size=size)
return book

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 시작 »
- 모듈 코드 »
- calibre.ebooks.metadata.book.base

calibre.ebooks.metadata.book.base의 소스 코드

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import copy import traceback from contextlib import suppress from calibre import prints from calibre.constants import DEBUG from calibre.ebooks.metadata.book import ALL_METADATA_FIELDS, SC_COPYABLE_FIELDS, SC_FIELDS_COPY_NOT_NULL, STANDARD_METADATA_FIELDS, TOP_LEVEL_IDENTIFIERS from calibre.library.field_metadata import FieldMetadata from calibre.utils.icu import lower as icu_lower from calibre.utils.icu import sort_key from calibre.utils.localization import ngettext

# Special sets used to optimize the performance of getting and setting
# attributes on Metadata objects
SIMPLE_GET = frozenset(STANDARD_METADATA_FIELDS - TOP_LEVEL_IDENTIFIERS)
SIMPLE_SET = frozenset(SIMPLE_GET - {'identifiers'})

def human_readable(size, precision=2):
''' Convert a size in bytes into megabytes '''
ans = size/(1024*1024)
if ans < 0.1:
return '<0.1 MB'
return ('%.'+str(precision)+'f'+ ' MB') % ans

NULL_VALUES = {
'user_metadata': {},
'cover_data' : (None, None),
'tags' : [],
'identifiers' : {},
'languages' : [],
'device_collections': [],
'author_sort_map': {},
'authors' : [_('Unknown')],
'author_sort' : _('Unknown'),
'title' : _('Unknown'),
'user_categories' : {},
'link_maps' : {},
'language' : 'und',
}

field_metadata = FieldMetadata()

def reset_field_metadata():
global field_metadata field_metadata = FieldMetadata()

def ck(typ):
return icu_lower(typ).strip().replace(':', '').replace(',', '')

def cv(val):
return val.strip().replace(',', '|')

[문서]
class Metadata:
'''
A class representing all the metadata for a book. The various standard metadata fields are available as attributes of this object. You can also stick arbitrary attributes onto this object.

Metadata from custom columns should be accessed via the get() method, passing in the lookup name for the column, for example: "#mytags".

Use the :meth:`is_null` method to test if a field is null.

This object also has functions to format fields into strings.

The list of standard metadata fields grows with time is in
:data:`STANDARD_METADATA_FIELDS`.

Please keep the method based API of this class to a minimum. Every method becomes a reserved field name.
'''
__calibre_serializable__ = True def __init__(self, title, authors=(_('Unknown'),), other=None, template_cache=None, formatter=None):
'''
@param title: title or ``_('Unknown')``
@param authors: List of strings or []
@param other: None or a metadata object
'''
_data = copy.deepcopy(NULL_VALUES)
_data.pop('language')
object.__setattr__(self, '_data', _data)
if other is not None:
self.smart_update(other)
else:
if title:
self.title = title if authors:
# List of strings or []
self.author = list(authors) if authors else [] # Needed for backward compatibility self.authors = list(authors) if authors else []
from calibre.ebooks.metadata.book.formatter import SafeFormat self.formatter = SafeFormat() if formatter is None else formatter self.template_cache = template_cache

[문서]
def is_null(self, field):
'''
Return True if the value of field is null in this object.
'null' means it is unknown or evaluates to False. So a title of
_('Unknown') is null or a language of 'und' is null.

Be careful with numeric fields since this will return True for zero as well as None.

Also returns True if the field does not exist.
'''
try:
val = getattr(self, field, None)
return not val or val == NULL_VALUES.get(field)
except Exception:
return True
def set_null(self, field):
null_val = copy.copy(NULL_VALUES.get(field))
setattr(self, field, null_val)

def __getattribute__(self, field):
_data = object.__getattribute__(self, '_data')
if field in SIMPLE_GET:
return _data.get(field, None)
if field in TOP_LEVEL_IDENTIFIERS:
return _data.get('identifiers').get(field, None)
if field == 'language':
try:
return _data.get('languages', [])[0]
except Exception:
return NULL_VALUES['language']
try:
return object.__getattribute__(self, field)
except AttributeError:
pass if field in _data['user_metadata']:
d = _data['user_metadata'][field]
val = d['#value#']
if val is None and d['datatype'] == 'composite':
from calibre.utils.formatter import TEMPLATE_ERROR d['#value#'] = 'RECURSIVE_COMPOSITE FIELD (Metadata) ' + field val = d['#value#'] = self.formatter.safe_format( d['display']['composite_template'], self,
TEMPLATE_ERROR, self, column_name=field, template_cache=self.template_cache).strip()
return val if field.startswith('#') and field.endswith('_index'):
try:
return self.get_extra(field[:-6])
except Exception:
pass raise AttributeError(
'Metadata object has no attribute named: '+ repr(field))

def __setattr__(self, field, val, extra=None):
_data = object.__getattribute__(self, '_data')
if field in SIMPLE_SET:
if val is None:
val = copy.copy(NULL_VALUES.get(field, None))
_data[field] = val elif field in TOP_LEVEL_IDENTIFIERS:
field, val = self._clean_identifier(field, val)
identifiers = _data['identifiers']
identifiers.pop(field, None)
if val:
identifiers[field] = val elif field == 'identifiers':
if not val:
val = copy.copy(NULL_VALUES.get('identifiers', None))
self.set_identifiers(val)
elif field == 'language':
langs = []
if val and val.lower() != 'und':
langs = [val]
_data['languages'] = langs elif field in _data['user_metadata']:
d = _data['user_metadata'][field]
d['#value#'] = val d['#extra#'] = extra else:
# You are allowed to stick arbitrary attributes onto this object as
# long as they don't conflict with global or user metadata names
# Don't abuse this privilege self.__dict__[field] = val def __iter__(self):
return iter(object.__getattribute__(self, '_data'))

def has_key(self, key):
return key in STANDARD_METADATA_FIELDS or key in object.__getattribute__(self, '_data')['user_metadata']

def _evaluate_all_composites(self):
custom_fields = object.__getattribute__(self, '_data')['user_metadata']
for field in custom_fields:
self._evaluate_composite(field)

def _evaluate_composite(self, field):
f = object.__getattribute__(self, '_data')['user_metadata'].get(field, None)
if f is not None:
if f['datatype'] == 'composite' and f['#value#'] is None:
self.get(field)

[문서]
def deepcopy(self, class_generator=lambda: Metadata(None)):
''' Do not use this method unless you know what you are doing, if you want to create a simple clone of this object, use :meth:`deepcopy_metadata` instead. Class_generator must be a function that returns an instance of Metadata or a subclass of it.'''
# We don't need to evaluate all the composites here because we
# are returning a "real" Metadata instance that has __get_attribute__.
m = class_generator()
if not isinstance(m, Metadata):
return None object.__setattr__(m, '__dict__', copy.deepcopy(self.__dict__))
return m
def deepcopy_metadata(self):
# We don't need to evaluate all the composites here because we
# are returning a "real" Metadata instance that has __get_attribute__.
m = Metadata(None)
object.__setattr__(m, '_data', copy.deepcopy(object.__getattribute__(self, '_data')))
# Also copy these two top-level attributes as they can appear in templates.
with suppress(AttributeError):
object.__setattr__(m, 'id', copy.copy(self.__getattribute__('id')))
with suppress(AttributeError):
object.__setattr__(m, 'has_cover', copy.copy(self.__getattribute__('has_cover')))
return m def get(self, field, default=None):
try:
return self.__getattribute__(field)
except AttributeError:
return default def get_extra(self, field, default=None):
# Don't need to evaluate all composites because a composite can't have
# an extra value
_data = object.__getattribute__(self, '_data')
if field in _data['user_metadata']:
try:
return _data['user_metadata'][field]['#extra#']
except Exception:
return default raise AttributeError(
'Metadata object has no attribute named: '+ repr(field))

def set(self, field, val, extra=None):
self.__setattr__(field, val, extra)

[문서]
def get_identifiers(self):
'''
Return a copy of the identifiers dictionary.
The dict is small, and the penalty for using a reference where a copy is needed is large. Also, we don't want any manipulations of the returned dict to show up in the book.
'''
ans = object.__getattribute__(self, '_data')['identifiers']
if not ans:
ans = {}
return copy.deepcopy(ans)

def _clean_identifier(self, typ, val):
if typ:
typ = ck(typ)
if val:
val = cv(val)
return typ, val

[문서]
def set_identifiers(self, identifiers):
'''
Set all identifiers. Note that if you previously set ISBN, calling this method will delete it.
'''
cleaned = {ck(k):cv(v) for k, v in identifiers.items() if k and v}
object.__getattribute__(self, '_data')['identifiers'] = cleaned

[문서]
def set_identifier(self, typ, val):
'If val is empty, deletes identifier of type typ'
typ, val = self._clean_identifier(typ, val)
if not typ:
return identifiers = object.__getattribute__(self, '_data')['identifiers']

identifiers.pop(typ, None)
if val:
identifiers[typ] = val
def has_identifier(self, typ):
identifiers = object.__getattribute__(self, '_data')['identifiers']
return typ in identifiers

# field-oriented interface. Intended to be the same as in LibraryDatabase

[문서]
def standard_field_keys(self):
'''
return a list of all possible keys, even if this book doesn't have them
'''
return STANDARD_METADATA_FIELDS

[문서]
def custom_field_keys(self):
'''
return a list of the custom fields in this book
'''
return iter(object.__getattribute__(self, '_data')['user_metadata'])

[문서]
def all_field_keys(self):
'''
All field keys known by this instance, even if their value is None
'''
_data = object.__getattribute__(self, '_data')
return frozenset(ALL_METADATA_FIELDS.union(frozenset(_data['user_metadata'])))

[문서]
def metadata_for_field(self, key):
'''
return metadata describing a standard or custom field.
'''
if key not in self.custom_field_keys():
return self.get_standard_metadata(key, make_copy=False)
return self.get_user_metadata(key, make_copy=False)

[문서]
def all_non_none_fields(self):
'''
Return a dictionary containing all non-None metadata fields, including the custom ones.
'''
result = {}
_data = object.__getattribute__(self, '_data')
for attr in STANDARD_METADATA_FIELDS:
v = _data.get(attr, None)
if v is not None:
result[attr] = v
# separate these because it uses the self.get(), not _data.get()
for attr in TOP_LEVEL_IDENTIFIERS:
v = self.get(attr, None)
if v is not None:
result[attr] = v for attr in _data['user_metadata']:
v = self.get(attr, None)
if v is not None:
result[attr] = v if _data['user_metadata'][attr]['datatype'] == 'series':
result[attr+'_index'] = _data['user_metadata'][attr]['#extra#']
return result

# End of field-oriented interface

# Extended interfaces. These permit one to get copies of metadata dictionaries, and to
# get and set custom field metadata

[문서]
def get_standard_metadata(self, field, make_copy):
'''
return field metadata from the field if it is there. Otherwise return
None. field is the key name, not the label. Return a copy if requested, just in case the user wants to change values in the dict.
'''
if field in field_metadata and field_metadata[field]['kind'] == 'field':
if make_copy:
return copy.deepcopy(field_metadata[field])
return field_metadata[field]
return None

[문서]
def get_all_standard_metadata(self, make_copy):
'''
return a dict containing all the standard field metadata associated with the book.
'''
if not make_copy:
return field_metadata res = {}
for k in field_metadata:
if field_metadata[k]['kind'] == 'field':
res[k] = copy.deepcopy(field_metadata[k])
return res

[문서]
def get_all_user_metadata(self, make_copy):
'''
return a dict containing all the custom field metadata associated with the book.
'''
# Must evaluate all composites because we are returning a dict, not a
# Metadata instance self._evaluate_all_composites()
_data = object.__getattribute__(self, '_data')
user_metadata = _data['user_metadata']
if not make_copy:
return user_metadata res = {}
for k in user_metadata:
res[k] = copy.deepcopy(user_metadata[k])
return res

[문서]
def get_user_metadata(self, field, make_copy):
'''
return field metadata from the object if it is there. Otherwise return
None. field is the key name, not the label. Return a copy if requested, just in case the user wants to change values in the dict.
'''
_data = object.__getattribute__(self, '_data')['user_metadata']
if field in _data:
# Must evaluate the field because it might be a composite. It won't
# be evaluated on demand because we are returning its dict, not a
# Metadata instance self._evaluate_composite(field)
if make_copy:
return copy.deepcopy(_data[field])
return _data[field]
return None

[문서]
def set_all_user_metadata(self, metadata):
'''
store custom field metadata into the object. Field is the key name not the label
'''
if metadata is None:
traceback.print_stack()
return um = {}
for key, meta in metadata.items():
m = meta.copy()
if '#value#' not in m:
if m['datatype'] == 'text' and m['is_multiple']:
m['#value#'] = []
else:
m['#value#'] = None um[key] = m
_data = object.__getattribute__(self, '_data')
_data['user_metadata'] = um

[문서]
def set_user_metadata(self, field, metadata):
'''
store custom field metadata for one column into the object. Field is the key name not the label
'''
if field is not None:
if not field.startswith('#'):
raise AttributeError( f"Custom field name {field!r} must begin with '#'")
if metadata is None:
traceback.print_stack()
return m = dict(metadata)
# Copying the elements should not be necessary. The objects referenced
# in the dict should not change. Of course, they can be replaced.
# for k,v in metadata.items():
# m[k] = copy.copy(v)
if '#value#' not in m:
if m['datatype'] == 'text' and m['is_multiple']:
m['#value#'] = []
else:
m['#value#'] = None
_data = object.__getattribute__(self, '_data')
_data['user_metadata'][field] = m

[문서]
def remove_stale_user_metadata(self, other_mi):
'''
Remove user metadata keys (custom column keys) if they don't exist in 'other_mi', which must be a metadata object
'''
me = self.get_all_user_metadata(make_copy=False)
other = set(other_mi.custom_field_keys())
new = {}
for k,v in me.items():
if k in other:
new[k] = v self.set_all_user_metadata(new)

[문서]
def template_to_attribute(self, other, ops):
'''
Takes a list [(src,dest), (src,dest)], evaluates the template in the context of other, then copies the result to self[dest]. This is on a best-efforts basis. Some assignments can make no sense.
'''
if not ops:
return from calibre.ebooks.metadata.book.formatter import SafeFormat formatter = SafeFormat()
for op in ops:
try:
src = op[0]
dest = op[1]
val = formatter.safe_format(src, other, 'PLUGBOARD TEMPLATE ERROR', other)
if dest == 'tags':
self.set(dest, [f.strip() for f in val.split(',') if f.strip()])
elif dest == 'authors':
self.set(dest, [f.strip() for f in val.split('&') if f.strip()])
else:
self.set(dest, val)
except Exception:
if DEBUG:
traceback.print_exc()

# Old Metadata API {{{ def print_all_attributes(self):
for x in STANDARD_METADATA_FIELDS:
prints(f'{x}:', getattr(self, x, 'None'))
for x in self.custom_field_keys():
meta = self.get_user_metadata(x, make_copy=False)
if meta is not None:
prints(x, meta)
prints('--------------')

[문서]
def smart_update(self, other, replace_metadata=False):
'''
Merge the information in `other` into self. In case of conflicts, the information in `other` takes precedence, unless the information in `other` is NULL.
'''
def copy_not_none(dest, src, attr):
v = getattr(src, attr, None)
if v is not None and v != NULL_VALUES.get(attr, None):
setattr(dest, attr, copy.deepcopy(v))

unknown = _('Unknown')
if other.title and other.title != unknown:
self.title = other.title if hasattr(other, 'title_sort'):
self.title_sort = other.title_sort if other.authors and ( other.authors[0] != unknown or ( not self.authors or ( len(self.authors) == 1 and self.authors[0] == unknown and getattr(self, 'author_sort', None) == unknown
)
)
):
self.authors = list(other.authors)
if hasattr(other, 'author_sort_map'):
self.author_sort_map = dict(other.author_sort_map)
if hasattr(other, 'author_sort'):
self.author_sort = other.author_sort if replace_metadata:
# SPECIAL_FIELDS = frozenset(['lpath', 'size', 'comments', 'thumbnail'])
for attr in SC_COPYABLE_FIELDS:
setattr(self, attr, getattr(other, attr, 1.0 if attr == 'series_index' else None))
self.tags = other.tags self.cover_data = getattr(other, 'cover_data',
NULL_VALUES['cover_data'])
self.set_all_user_metadata(other.get_all_user_metadata(make_copy=True))
for x in SC_FIELDS_COPY_NOT_NULL:
copy_not_none(self, other, x)
if callable(getattr(other, 'get_identifiers', None)):
self.set_identifiers(other.get_identifiers())
# language is handled below else:
for attr in SC_COPYABLE_FIELDS:
copy_not_none(self, other, attr)
for x in SC_FIELDS_COPY_NOT_NULL:
copy_not_none(self, other, x)

if other.tags:
# Case-insensitive but case preserving merging lotags = [t.lower() for t in other.tags]
lstags = [t.lower() for t in self.tags]
ot, st = map(frozenset, (lotags, lstags))
for t in st.intersection(ot):
sidx = lstags.index(t)
oidx = lotags.index(t)
self.tags[sidx] = other.tags[oidx]
self.tags += [t for t in other.tags if t.lower() in ot-st]

if getattr(other, 'cover_data', False):
other_cover = other.cover_data[-1]
self_cover = self.cover_data[-1] if self.cover_data else b''
if not self_cover:
self_cover = b''
if not other_cover:
other_cover = b''
if len(other_cover) > len(self_cover):
self.cover_data = other.cover_data if callable(getattr(other, 'custom_field_keys', None)):
for x in other.custom_field_keys():
meta = other.get_user_metadata(x, make_copy=True)
if meta is not None:
self_tags = self.get(x, [])
if isinstance(self_tags, (str, bytes)):
self_tags = []
self.set_user_metadata(x, meta) # get... did the deepcopy other_tags = other.get(x, [])
if meta['datatype'] == 'text' and meta['is_multiple']:
# Case-insensitive but case preserving merging lotags = [t.lower() for t in other_tags]
try:
lstags = [t.lower() for t in self_tags]
except TypeError:
# Happens if x is not a text, is_multiple field
# on self lstags = []
self_tags = []
ot, st = map(frozenset, (lotags, lstags))
for t in st.intersection(ot):
sidx = lstags.index(t)
oidx = lotags.index(t)
self_tags[sidx] = other_tags[oidx]
self_tags += [t for t in other_tags if t.lower() in ot-st]
setattr(self, x, self_tags)

my_comments = getattr(self, 'comments', '')
other_comments = getattr(other, 'comments', '')
if not my_comments:
my_comments = ''
if not other_comments:
other_comments = ''
if len(other_comments.strip()) > len(my_comments.strip()):
self.comments = other_comments

# Copy all the non-none identifiers if callable(getattr(other, 'get_identifiers', None)):
d = self.get_identifiers()
s = other.get_identifiers()
d.update([v for v in s.items() if v[1] is not None])
self.set_identifiers(d)
else:
# other structure not Metadata. Copy the top-level identifiers for attr in TOP_LEVEL_IDENTIFIERS:
copy_not_none(self, other, attr)

other_lang = getattr(other, 'languages', [])
if other_lang and other_lang != ['und']:
self.languages = list(other_lang)
if not getattr(self, 'series', None):
self.series_index = Nonedef format_series_index(self, val=None):
from calibre.ebooks.metadata import fmt_sidx v = self.series_index if val is None else val try:
x = float(v)
except Exception:
x = 1 return fmt_sidx(x)

def authors_from_string(self, raw):
from calibre.ebooks.metadata import string_to_authors self.authors = string_to_authors(raw)

def format_authors(self):
from calibre.ebooks.metadata import authors_to_string return authors_to_string(self.authors)

def format_tags(self):
return ', '.join([str(t) for t in sorted(self.tags, key=sort_key)])

def format_rating(self, v=None, divide_by=1):
if v is None:
if self.rating is not None:
return str(self.rating/divide_by)
return 'None'
return str(v/divide_by)

[문서]
def format_field(self, key, series_with_index=True):
'''
Returns the tuple (display_name, formatted_value)
'''
name, val, _ign, _ign = self.format_field_extended(key, series_with_index)
return name, val
def format_field_extended(self, key, series_with_index=True):
from calibre.ebooks.metadata import authors_to_string
'''
returns the tuple (display_name, formatted_value, original_value, field_metadata)
'''
from calibre.utils.date import format_date

# Handle custom series index if key.startswith('#') and key.endswith('_index'):
tkey = key[:-6] # strip the _index cmeta = self.get_user_metadata(tkey, make_copy=False)
if cmeta and cmeta['datatype'] == 'series':
if self.get(tkey):
res = self.get_extra(tkey)
return (str(cmeta['name']+'_index'), self.format_series_index(res), res, cmeta)
else:
return (str(cmeta['name']+'_index'), '', '', cmeta)

if key in self.custom_field_keys():
res = self.get(key, None) # get evaluates all necessary composites cmeta = self.get_user_metadata(key, make_copy=False)
name = str(cmeta['name'])
if res is None or res == '': # can't check "not res" because of numeric fields return (name, res, None, None)
orig_res = res datatype = cmeta['datatype']
if datatype == 'text' and cmeta['is_multiple']:
res = cmeta['is_multiple']['list_to_ui'].join(res)
elif datatype == 'series' and series_with_index:
if self.get_extra(key) is not None:
res = res + f' [{self.format_series_index(val=self.get_extra(key))}]'
elif datatype == 'datetime':
res = format_date(res, cmeta['display'].get('date_format','dd MMM yyyy'))
elif datatype == 'bool':
res = _('Yes') if res else _('No')
elif datatype == 'rating':
res = f'{res/2:.2g}'
elif datatype in ['int', 'float']:
try:
fmt = cmeta['display'].get('number_format', None)
res = fmt.format(res)
except Exception:
pass return (name, str(res), orig_res, cmeta)

# convert top-level ids into their value if key in TOP_LEVEL_IDENTIFIERS:
fmeta = field_metadata['identifiers']
name = key res = self.get(key, None)
return (name, res, res, fmeta)

# Translate aliases into the standard field name fmkey = field_metadata.search_term_to_field_key(key)
if fmkey in field_metadata and field_metadata[fmkey]['kind'] == 'field':
res = self.get(key, None)
fmeta = field_metadata[fmkey]
name = str(fmeta['name'])
if res is None or res == '':
return (name, res, None, None)
orig_res = res name = str(fmeta['name'])
datatype = fmeta['datatype']
if key == 'authors':
res = authors_to_string(res)
elif key == 'series_index':
res = self.format_series_index(res)
elif datatype == 'text' and fmeta['is_multiple']:
if isinstance(res, dict):
res = [k + ':' + v for k,v in res.items()]
res = fmeta['is_multiple']['list_to_ui'].join(sorted(filter(None, res), key=sort_key))
elif datatype == 'series' and series_with_index:
res = res + f' [{self.format_series_index()}]'
elif datatype == 'datetime':
res = format_date(res, fmeta['display'].get('date_format','dd MMM yyyy'))
elif datatype == 'rating':
res = f'{res/2:.2g}'
elif key == 'size':
res = human_readable(res)
elif key == 'pages':
match res:
case -3:
res = _('DRM')
case -2:
res = _('Error')
case -1:
res = _('None')
return (name, str(res), orig_res, fmeta)

if kv := self.get(key, None):
return (key, str(kv), kv, None)

return (None, None, None, None)

def __unicode__representation__(self):
'''
A string representation of this object, suitable for printing to console
'''
from calibre.ebooks.metadata import authors_to_string from calibre.utils.date import isoformat ans = []

def fmt(x, y):
ans.append(f'{x:<20}: {y}')

fmt('Title', self.title)
if self.title_sort:
fmt('Title sort', self.title_sort)
if self.authors:
fmt('Author(s)', authors_to_string(self.authors) +
((' [' + self.author_sort + ']')
if self.author_sort and self.author_sort != _('Unknown') else ''))
if self.publisher:
fmt('Publisher', self.publisher)
if getattr(self, 'book_producer', False):
fmt('Book Producer', self.book_producer)
if self.tags:
fmt('Tags', ', '.join([str(t) for t in self.tags]))
if self.series:
fmt('Series', self.series + f' #{self.format_series_index()}')
if not self.is_null('languages'):
fmt('Languages', ', '.join(self.languages))
if self.rating is not None:
fmt('Rating', (f'{float(self.rating)/2:.2g}') if self.rating else '')
if self.timestamp is not None:
fmt('Timestamp', isoformat(self.timestamp))
if self.pubdate is not None:
fmt('Published', isoformat(self.pubdate))
if self.rights is not None:
fmt('Rights', str(self.rights))
if self.identifiers:
fmt('Identifiers', ', '.join([f'{k}:{v}' for k, v in self.identifiers.items()]))
if self.comments:
fmt('Comments', self.comments)

for key in self.custom_field_keys():
val = self.get(key, None)
if val:
name, val = self.format_field(key)
fmt(name, str(val))
return '\n'.join(ans)

[문서]
def to_html(self):
'''
A HTML representation of this object.
'''
from calibre.ebooks.metadata import authors_to_string from calibre.utils.date import isoformat ans = [(_('Title'), str(self.title))]
ans += [(_('Author(s)'), (authors_to_string(self.authors) if self.authors else _('Unknown')))]
ans += [(_('Publisher'), str(self.publisher))]
ans += [(_('Producer'), str(self.book_producer))]
ans += [(_('Comments'), str(self.comments))]
ans += [('ISBN', str(self.isbn))]
ans += [(_('Tags'), ', '.join([str(t) for t in self.tags]))]
if self.series:
ans += [(ngettext('Series', 'Series', 1), str(self.series) + f' #{self.format_series_index()}')]
ans += [(_('Languages'), ', '.join(self.languages))]
if self.timestamp is not None:
ans += [(_('Timestamp'), str(isoformat(self.timestamp, as_utc=False, sep=' ')))]
if self.pubdate is not None:
ans += [(_('Published'), str(isoformat(self.pubdate, as_utc=False, sep=' ')))]
if self.rights is not None:
ans += [(_('Rights'), str(self.rights))]
for key in self.custom_field_keys():
val = self.get(key, None)
if val:
name, val = self.format_field(key)
ans += [(name, val)]
for i, x in enumerate(ans):
ans[i] = '<tr><td><b>{}</b></td><td>{}</td></tr>'.format(*x)
return '<table>{}</table>'.format('\n'.join(ans))__str__ = __unicode__representation__ def __nonzero__(self):
return bool(self.title or self.author or self.comments or self.tags)
__bool__ = __nonzero__# }}}

def field_from_string(field, raw, field_metadata):
''' Parse the string raw to return an object that is suitable for calling set() on a Metadata object. '''
dt = field_metadata['datatype']
val = object if dt in {'int', 'float'}:
val = int(raw) if dt == 'int' else float(raw)
elif dt == 'rating':
val = float(raw) * 2 elif dt == 'datetime':
from calibre.utils.iso8601 import parse_iso8601 try:
val = parse_iso8601(raw, require_aware=True)
except Exception:
from calibre.utils.date import parse_only_date val = parse_only_date(raw)
elif dt == 'bool':
if raw.lower() in {'true', 'yes', 'y'}:
val = True elif raw.lower() in {'false', 'no', 'n'}:
val = False else:
raise ValueError(f'Unknown value for {field}: {raw}')
elif dt == 'text':
ism = field_metadata['is_multiple']
if ism:
val = [x.strip() for x in raw.split(ism['ui_to_list'])]
if field == 'identifiers':
val = {x.partition(':')[0]:x.partition(':')[-1] for x in val}
elif field == 'languages':
from calibre.utils.localization import canonicalize_lang val = [canonicalize_lang(x) for x in val]
val = [x for x in val if x]
if val is object:
val = raw return val def get_model_metadata_instance():
'''
Get a metadata instance that contains all the fields in the current database with the fields to a plausible value. This function must only be used in the GUI thread.
'''
from calibre.gui2 import is_gui_thread if not is_gui_thread():
raise ValueError('get_model_metadata_instance() must only be used in the GUI thread')

mi = Metadata(_('Title'), [_('Author')])
mi.author_sort = _('Author Sort')
mi.series = ngettext('Series', 'Series', 1)
mi.series_index = 3 mi.rating = 4.0 mi.tags = [_('Tag 1'), _('Tag 2')]
mi.languages = ['eng']
mi.id = -1 from calibre.gui2.ui import get_gui from calibre.utils.date import DEFAULT_DATE fm = get_gui().current_db.new_api.field_metadata mi.set_all_user_metadata(fm.custom_field_metadata())
for col in mi.get_all_user_metadata(False):
if fm[col]['datatype'] == 'datetime':
mi.set(col, DEFAULT_DATE)
elif fm[col]['datatype'] in ('int', 'float', 'rating'):
mi.set(col, 2)
elif fm[col]['datatype'] == 'bool':
mi.set(col, False)
elif fm[col]['is_multiple']:
mi.set(col, [col])
else:
mi.set(col, col, 1)
return mi

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 시작 »
- 모듈 코드 »
- calibre.ebooks.metadata.sources.base

calibre.ebooks.metadata.sources.base의 소스 코드

#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai from __future__ import absolute_import, division, print_function, unicode_literals

__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import re import threading from functools import total_ordering from calibre import browser, random_user_agent from calibre.customize import Plugin from calibre.ebooks.metadata import check_isbn from calibre.ebooks.metadata.author_mapper import cap_author_token from calibre.utils.localization import canonicalize_lang, get_lang from polyglot.builtins import cmp, iteritems def create_log(ostream=None):
from calibre.utils.logging import FileStream, ThreadSafeLog log = ThreadSafeLog(level=ThreadSafeLog.DEBUG)
log.outputs = [FileStream(ostream)]
return log

# Comparing Metadata objects for relevance {{{ words = ('the', 'a', 'an', 'of', 'and')
prefix_pat = re.compile(r'^(%s)\s+'%('|'.join(words)))
trailing_paren_pat = re.compile(r'\(.*\)$')
whitespace_pat = re.compile(r'\s+')

def cleanup_title(s):
if not s:
s = _('Unknown')
s = s.strip().lower()
s = prefix_pat.sub(' ', s)
s = trailing_paren_pat.sub('', s)
s = whitespace_pat.sub(' ', s)
return s.strip()

[문서]
@total_ordering class InternalMetadataCompareKeyGen:
'''
Generate a sort key for comparison of the relevance of Metadata objects, given a search query. This is used only to compare results from the same metadata source, not across different sources.

The sort key ensures that an ascending order sort is a sort by order of decreasing relevance.

The algorithm is:

* Prefer results that have at least one identifier the same as for the query
* Prefer results with a cached cover URL
* Prefer results with all available fields filled in
* Prefer results with the same language as the current user interface language
* Prefer results that are an exact title match to the query
* Prefer results with longer comments (greater than 10% longer)
* Use the relevance of the result as reported by the metadata source's search engine
'''

def __init__(self, mi, source_plugin, title, authors, identifiers):
same_identifier = 2 idents = mi.get_identifiers()
for k, v in iteritems(identifiers):
if idents.get(k) == v:
same_identifier = 1 break all_fields = 1 if source_plugin.test_fields(mi) is None else 2 exact_title = 1 if title and \ cleanup_title(title) == cleanup_title(mi.title) else 2 language = 1 if mi.language:
mil = canonicalize_lang(mi.language)
if mil != 'und' and mil != canonicalize_lang(get_lang()):
language = 2 has_cover = 2 if (not source_plugin.cached_cover_url_is_reliable or source_plugin.get_cached_cover_url(mi.identifiers) is None) else 1 self.base = (same_identifier, has_cover, all_fields, language, exact_title)
self.comments_len = len((mi.comments or '').strip())
self.extra = getattr(mi, 'source_relevance', 0)

def compare_to_other(self, other):
a = cmp(self.base, other.base)
if a != 0:
return a cx, cy = self.comments_len, other.comments_len if cx and cy:
t = (cx + cy) / 20 delta = cy - cx if abs(delta) > t:
return -1 if delta < 0 else 1 return cmp(self.extra, other.extra)

def __eq__(self, other):
return self.compare_to_other(other) == 0 def __ne__(self, other):
return self.compare_to_other(other) != 0 def __lt__(self, other):
return self.compare_to_other(other) < 0 def __le__(self, other):
return self.compare_to_other(other) <= 0 def __gt__(self, other):
return self.compare_to_other(other) > 0 def __ge__(self, other):
return self.compare_to_other(other) >= 0# }}}

def get_cached_cover_urls(mi):
from calibre.customize.ui import metadata_plugins plugins = list(metadata_plugins(['identify']))
for p in plugins:
url = p.get_cached_cover_url(mi.identifiers)
if url:
yield (p, url)

def dump_caches():
from calibre.customize.ui import metadata_plugins return {p.name:p.dump_caches() for p in metadata_plugins(['identify'])}

def load_caches(dump):
from calibre.customize.ui import metadata_plugins plugins = list(metadata_plugins(['identify']))
for p in plugins:
cache = dump.get(p.name, None)
if cache:
p.load_caches(cache)

def fixauthors(authors):
if not authors:
return authors ans = []
for x in authors:
ans.append(' '.join(map(cap_author_token, x.split())))
return ans def fixcase(x):
if x:
from calibre.utils.titlecase import titlecase x = titlecase(x)
return x class Option:
__slots__ = ('choices', 'default', 'desc', 'label', 'name', 'type')

def __init__(self, name, type_, default, label, desc, choices=None):
'''
:param name: The name of this option. Must be a valid python identifier
:param type_: The type of this option, one of ('number', 'string',
'bool', 'choices')
:param default: The default value for this option
:param label: A short (few words) description of this option
:param desc: A longer description of this option
:param choices: A dict of possible values, used only if type='choices'.
dict is of the form {key:human readable label, ...}
'''
self.name, self.type, self.default, self.label, self.desc = (name, type_, default, label, desc)
if choices and not isinstance(choices, dict):
choices = {x: x for x in choices}
self.choices = choices

[문서]
class Source(Plugin):

type = _('Metadata source')
author = 'Kovid Goyal'

supported_platforms = ['windows', 'osx', 'linux']

#: Set of capabilities supported by this plugin.
#: Useful capabilities are: 'identify', 'cover'
capabilities = frozenset()

#: List of metadata fields that can potentially be download by this plugin
#: during the identify phase touched_fields = frozenset()

#: Set this to True if your plugin returns HTML formatted comments has_html_comments = False

#: Setting this to True means that the browser object will indicate
#: that it supports gzip transfer encoding. This can speedup downloads
#: but make sure that the source actually supports gzip transfer encoding
#: correctly first supports_gzip_transfer_encoding = False

#: Set this to True to ignore HTTPS certificate errors when connecting
#: to this source.
ignore_ssl_errors = False

#: Cached cover URLs can sometimes be unreliable (i.e. the download could
#: fail or the returned image could be bogus). If that is often the case
#: with this source, set to False cached_cover_url_is_reliable = True

#: A list of :class:`Option` objects. They will be used to automatically
#: construct the configuration widget for this plugin options = ()

#: A string that is displayed at the top of the config widget for this
#: plugin config_help_message = None

#: If True this source can return multiple covers for a given query can_get_multiple_covers = False

#: If set to True covers downloaded by this plugin are automatically trimmed.
auto_trim_covers = False

#: If set to True, and this source returns multiple results for a query,
#: some of which have ISBNs and some of which do not, the results without
#: ISBNs will be ignored prefer_results_with_isbn = True def __init__(self, *args, **kwargs):
Plugin.__init__(self, *args, **kwargs)
self.running_a_test = False # Set to True when using identify_test()
self._isbn_to_identifier_cache = {}
self._identifier_to_cover_url_cache = {}
self.cache_lock = threading.RLock()
self._config_obj = None self._browser = None self.prefs.defaults['ignore_fields'] = []
for opt in self.options:
self.prefs.defaults[opt.name] = opt.default

# Configuration {{{

[문서]
def is_configured(self):
'''
Return False if your plugin needs to be configured before it can be used. For example, it might need a username/password/API key.
'''
return True
def is_customizable(self):
return True

[문서]
def customization_help(self):
return 'This plugin can only be customized using the GUI'

[문서]
def config_widget(self):
from calibre.gui2.metadata.config import ConfigWidget return ConfigWidget(self)

[문서]
def save_settings(self, config_widget):
config_widget.commit()

@property def prefs(self):
if self._config_obj is None:
from calibre.utils.config import JSONConfig self._config_obj = JSONConfig('metadata_sources/%s.json'%self.name)
return self._config_obj
# }}}

# Browser {{{

@property def user_agent(self):
# Pass in an index to random_user_agent() to test with a particular
# user agent return random_user_agent()

@property def browser(self):
if self._browser is None:
self._browser = browser(user_agent=self.user_agent, verify_ssl_certificates=not self.ignore_ssl_errors)
if self.supports_gzip_transfer_encoding:
self._browser.set_handle_gzip(True)
return self._browser.clone_browser()

# }}}

# Caching {{{ def get_related_isbns(self, id_):
with self.cache_lock:
for isbn, q in iteritems(self._isbn_to_identifier_cache):
if q == id_:
yield isbn def cache_isbn_to_identifier(self, isbn, identifier):
with self.cache_lock:
self._isbn_to_identifier_cache[isbn] = identifier def cached_isbn_to_identifier(self, isbn):
with self.cache_lock:
return self._isbn_to_identifier_cache.get(isbn, None)

def cache_identifier_to_cover_url(self, id_, url):
with self.cache_lock:
self._identifier_to_cover_url_cache[id_] = url def cached_identifier_to_cover_url(self, id_):
with self.cache_lock:
return self._identifier_to_cover_url_cache.get(id_, None)

def dump_caches(self):
with self.cache_lock:
return {'isbn_to_identifier':self._isbn_to_identifier_cache.copy(),
'identifier_to_cover':self._identifier_to_cover_url_cache.copy()}

def load_caches(self, dump):
with self.cache_lock:
self._isbn_to_identifier_cache.update(dump['isbn_to_identifier'])
self._identifier_to_cover_url_cache.update(dump['identifier_to_cover'])

# }}}

# Utility functions {{{

[문서]
def get_author_tokens(self, authors, only_first_author=True):
'''
Take a list of authors and return a list of tokens useful for an
AND search query. This function tries to return tokens in first name middle names last name order, by assuming that if a comma is in the author name, the name is in lastname, other names form.
'''

if authors:
# Leave ' in there for Irish names remove_pat = re.compile(r'[!@#$%^&*()（）「」{}`~"\s\[\]/]')
replace_pat = re.compile(r'[-+.:;,，。；：]')
if only_first_author:
authors = authors[:1]
for au in authors:
has_comma = ',' in au au = replace_pat.sub(' ', au)
parts = au.split()
if has_comma:
# au probably in ln, fn form parts = parts[1:] + parts[:1]
for tok in parts:
tok = remove_pat.sub('', tok).strip()
if len(tok) > 2 and tok.lower() not in ('von', 'van',
_('Unknown').lower()):
yield tok

[문서]
def get_title_tokens(self, title, strip_joiners=True, strip_subtitle=False):
'''
Take a title and return a list of tokens useful for an AND search query.
Excludes connectives(optionally) and punctuation.
'''
if title:
# strip sub-titles if strip_subtitle:
subtitle = re.compile(r'([\(\[\{].*?[\)\]\}]|[/:\\].*$)')
if len(subtitle.sub('', title)) > 1:
title = subtitle.sub('', title)

title_patterns = [(re.compile(pat, re.IGNORECASE), repl) for pat, repl in
[
# Remove things like: (2010) (Omnibus) etc.
(r'(?i)[({\[](\d{4}|omnibus|anthology|hardcover|audiobook|audio\scd|paperback|turtleback|mass\s*market|edition|ed\.)[\])}]', ''),
# Remove any strings that contain the substring edition inside
# parentheses
(r'(?i)[({\[].*?(edition|ed.).*?[\]})]', ''),
# Remove commas used a separators in numbers
(r'(\d+),(\d+)', r'\1\2'),
# Remove hyphens only if they have whitespace before them
(r'(\s-)', ' '),
# Replace other special chars with a space
(r'''[:,;!@$%^&*(){}.`~"\s\[\]/]《》「」“”''', ' '),
]]

for pat, repl in title_patterns:
title = pat.sub(repl, title)

tokens = title.split()
for token in tokens:
token = token.strip().strip('"').strip("'")
if token and (not strip_joiners or token.lower() not in ('a',
'and', 'the', '&')):
yield token

[문서]
def split_jobs(self, jobs, num):
'Split a list of jobs into at most num groups, as evenly as possible'
groups = [[] for i in range(num)]
jobs = list(jobs)
while jobs:
for gr in groups:
try:
job = jobs.pop()
except IndexError:
break gr.append(job)
return [g for g in groups if g]

[문서]
def test_fields(self, mi):
'''
Return the first field from self.touched_fields that is null on the mi object
'''
for key in self.touched_fields:
if key.startswith('identifier:'):
key = key.partition(':')[-1]
if not mi.has_identifier(key):
return 'identifier: ' + key elif mi.is_null(key):
return key

[문서]
def clean_downloaded_metadata(self, mi):
'''
Call this method in your plugin's identify method to normalize metadata before putting the Metadata object into result_queue. You can of course, use a custom algorithm suited to your metadata source.
'''
docase = mi.language == 'eng' or mi.is_null('language')
if docase and mi.title:
mi.title = fixcase(mi.title)
mi.authors = fixauthors(mi.authors)
if mi.tags and docase:
mi.tags = list(map(fixcase, mi.tags))
mi.isbn = check_isbn(mi.isbn)

def download_multiple_covers(self, title, authors, urls, get_best_cover, timeout, result_queue, abort, log, prefs_name='max_covers'):
if not urls:
log('No images found for, title: %r and authors: %r'%(title, authors))
return import time from threading import Thread if prefs_name:
urls = urls[:self.prefs[prefs_name]]
if get_best_cover:
urls = urls[:1]
log('Downloading %d covers'%len(urls))
workers = [Thread(target=self.download_image, args=(u, timeout, log, result_queue)) for u in urls]
for w in workers:
w.daemon = True w.start()
alive = True start_time = time.time()
while alive and not abort.is_set() and time.time() - start_time < timeout:
alive = False for w in workers:
if w.is_alive():
alive = True break abort.wait(0.1)

def download_image(self, url, timeout, log, result_queue):
try:
ans = self.browser.open_novisit(url, timeout=timeout).read()
result_queue.put((self, ans))
log('Downloaded cover from: %s'%url)
except Exception:
self.log.exception('Failed to download cover from: %r'%url)

# }}}

# Metadata API {{{

[문서]
def get_book_url(self, identifiers):
'''
Return a 3-tuple or None. The 3-tuple is of the form:
(identifier_type, identifier_value, URL).
The URL is the URL for the book identified by identifiers at this source. identifier_type, identifier_value specify the identifier corresponding to the URL.
This URL must be browsable to by a human using a browser. It is meant to provide a clickable link for the user to easily visit the books page at this source.
If no URL is found, return None. This method must be quick, and consistent, so only implement it if it is possible to construct the URL from a known scheme given identifiers.
'''
return

[문서]
def get_book_url_name(self, idtype, idval, url):
'''
Return a human readable name from the return value of get_book_url().
'''
return self.name

[문서]
def get_book_urls(self, identifiers):
'''
Override this method if you would like to return multiple URLs for this book.
Return a list of 3-tuples. By default this method simply calls :func:`get_book_url`.
'''
data = self.get_book_url(identifiers)
if data is None:
return ()
return (data,)

[문서]
def get_cached_cover_url(self, identifiers):
'''
Return cached cover URL for the book identified by the identifiers dictionary or None if no such URL exists.

Note that this method must only return validated URLs, i.e. not URLS that could result in a generic cover image or a not found error.
'''
return

[문서]
def id_from_url(self, url):
'''
Parse a URL and return a tuple of the form:
(identifier_type, identifier_value).
If the URL does not match the pattern for the metadata source, return None.
'''
return

[문서]
def identify_results_keygen(self, title=None, authors=None, identifiers={}):
'''
Return a function that is used to generate a key that can sort Metadata objects by their relevance given a search query (title, authors, identifiers).

These keys are used to sort the results of a call to :meth:`identify`.

For details on the default algorithm see
:class:`InternalMetadataCompareKeyGen`. Re-implement this function in your plugin if the default algorithm is not suitable.
'''
def keygen(mi):
return InternalMetadataCompareKeyGen(mi, self, title, authors, identifiers)
return keygen

[문서]
def identify(self, log, result_queue, abort, title=None, authors=None, identifiers={}, timeout=30):
'''
Identify a book by its Title/Author/ISBN/etc.

If identifiers(s) are specified and no match is found and this metadata source does not store all related identifiers (for example, all ISBNs of a book), this method should retry with just the title and author
(assuming they were specified).

If this metadata source also provides covers, the URL to the cover should be cached so that a subsequent call to the get covers API with the same ISBN/special identifier does not need to get the cover URL again. Use the caching API for this.

Every Metadata object put into result_queue by this method must have a
`source_relevance` attribute that is an integer indicating the order in which the results were returned by the metadata source for this query.
This integer will be used by :meth:`compare_identify_results`. If the order is unimportant, set it to zero for every result.

Make sure that any cover/ISBN mapping information is cached before the
Metadata object is put into result_queue.

:param log: A log object, use it to output debugging information/errors
:param result_queue: A result Queue, results should be put into it.
Each result is a Metadata object
:param abort: If abort.is_set() returns True, abort further processing and return as soon as possible
:param title: The title of the book, can be None
:param authors: A list of authors of the book, can be None
:param identifiers: A dictionary of other identifiers, most commonly
{'isbn':'1234...'}
:param timeout: Timeout in seconds, no network request should hang for longer than timeout.
:return: None if no errors occurred, otherwise a unicode representation of the error suitable for showing to the user

'''
return

[문서]
def download_cover(self, log, result_queue, abort, title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False):
'''
Download a cover and put it into result_queue. The parameters all have the same meaning as for :meth:`identify`. Put (self, cover_data) into result_queue.

This method should use cached cover URLs for efficiency whenever possible. When cached data is not present, most plugins simply call identify and use its results.

If the parameter get_best_cover is True and this plugin can get multiple covers, it should only get the "best" one.
'''
pass

# }}}

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 시작 »
- 모듈 코드 »
- calibre.ebooks.oeb.polish.container

calibre.ebooks.oeb.polish.container의 소스 코드

#!/usr/bin/env python
# License: GPLv3 Copyright: 2013, Kovid Goyal <kovid at kovidgoyal.net> import errno import hashlib import logging import os import re import shutil import sys import unicodedata import uuid from collections import defaultdict from io import BytesIO from itertools import count from math import floor from urllib.parse import urlparse from css_parser import getUrls, replaceUrls from calibre import CurrentDir, walk from calibre.constants import iswindows from calibre.customize.ui import plugin_for_input_format, plugin_for_output_format from calibre.ebooks import escape_xpath_attr from calibre.ebooks.chardet import xml_to_unicode from calibre.ebooks.conversion.plugins.epub_input import ADOBE_OBFUSCATION, IDPF_OBFUSCATION, decrypt_font_data from calibre.ebooks.conversion.preprocess import CSSPreProcessor as cssp from calibre.ebooks.conversion.preprocess import HTMLPreProcessor from calibre.ebooks.metadata.opf3 import CALIBRE_PREFIX, ensure_prefix, items_with_property, read_prefixes from calibre.ebooks.metadata.utils import parse_opf_version from calibre.ebooks.mobi import MobiError from calibre.ebooks.mobi.reader.headers import MetadataHeader from calibre.ebooks.oeb.base import (
DC11_NS,
OEB_DOCS,
OEB_STYLES,
OPF,
OPF2_NS,
Manifest, itercsslinks, iterlinks, rewrite_links, serialize, urlquote, urlunquote,
)
from calibre.ebooks.oeb.parse_utils import NotHTML, parse_html from calibre.ebooks.oeb.polish.errors import DRMError, InvalidBook from calibre.ebooks.oeb.polish.parsing import decode_xml from calibre.ebooks.oeb.polish.parsing import parse as parse_html_tweak from calibre.ebooks.oeb.polish.utils import OEB_FONTS, CommentFinder, PositionFinder, adjust_mime_for_epub, guess_type, insert_self_closing, parse_css from calibre.ptempfile import PersistentTemporaryDirectory, PersistentTemporaryFile, TemporaryDirectory from calibre.utils.filenames import hardlink_file, make_long_path_useable, nlinks_file, retry_on_fail from calibre.utils.ipc.simple_worker import WorkerError, fork_job from calibre.utils.logging import default_log from calibre.utils.xml_parse import safe_xml_fromstring from calibre.utils.zipfile import ZipFile exists, join, relpath = os.path.exists, os.path.join, os.path.relpath
OPF_NAMESPACES = {'opf':OPF2_NS, 'dc':DC11_NS}
null = object()
OEB_FONTS # for plugin compat class CSSPreProcessor(cssp):

def __call__(self, data):
return self.MS_PAT.sub(self.ms_sub, data)

def clone_dir(src, dest):
' Clone a folder using hard links for the files, dest must already exist '
for x in os.listdir(src):
dpath = os.path.join(dest, x)
spath = os.path.join(src, x)
if os.path.isdir(spath):
os.mkdir(dpath)
clone_dir(spath, dpath)
else:
try:
hardlink_file(spath, dpath)
except Exception:
shutil.copy2(spath, dpath)

def clone_container(container, dest_dir, container_class=None):
' Efficiently clone a container using hard links '
dest_dir = os.path.abspath(os.path.realpath(dest_dir))
clone_data = container.clone_data(dest_dir)
container_class = container_class or type(container)
return container_class(log=container.log, clone_data=clone_data)

def name_to_abspath(name, root):
return os.path.abspath(join(root, *name.split('/')))

def abspath_to_name(path, root):
return relpath(os.path.abspath(path), root).replace(os.sep, '/')

def name_to_href(name, root, base=None, quote=urlquote):
fullpath = name_to_abspath(name, root)
basepath = root if base is None else os.path.dirname(name_to_abspath(base, root))
path = relpath(fullpath, basepath).replace(os.sep, '/')
return quote(path)

def href_to_name(href, root, base=None):
base = root if base is None else os.path.dirname(name_to_abspath(base, root))
try:
purl = urlparse(href)
except ValueError:
return None if purl.scheme or not purl.path:
return None href = urlunquote(purl.path)
if iswindows and ':' in href:
# path manipulations on windows fail for paths with : in them, so we
# assume all such paths are invalid/absolute paths.
return None fullpath = os.path.join(base, *href.split('/'))
try:
return unicodedata.normalize('NFC', abspath_to_name(fullpath, root))
except ValueError:
return None def seconds_to_timestamp(duration: float) -> str:
seconds = floor(duration)
float_part = duration - seconds hours = seconds // 3600 minutes = (seconds % 3600) // 60 seconds = seconds % 60 ans = f'{hours:02d}:{minutes:02d}:{seconds:02d}'
if float_part:
ans += f'{float_part:.20f}'.rstrip('0')[1:]
return ans class ContainerBase: # {{{
'''
A base class that implements just the parsing methods. Useful to create virtual containers for testing.
'''

#: The mode used to parse HTML and CSS (polishing uses tweak_mode=False and the editor uses tweak_mode=True)
tweak_mode = False def __init__(self, log=default_log):
self.log = log or default_log self.parsed_cache = {}
self.mime_map = {}
self.encoding_map = {}
self.html_preprocessor = HTMLPreProcessor()
self.css_preprocessor = CSSPreProcessor()

def guess_type(self, name):
' Return the expected mimetype for the specified file name based on its extension. '
return adjust_mime_for_epub(filename=name, opf_version=self.opf_version_parsed)

def decode(self, data, normalize_to_nfc=True):
'''
Automatically decode ``data`` into a ``unicode`` object.

:param normalize_to_nfc: Normalize returned unicode to the NFC normal form as is required by both the EPUB and AZW3 formats.
'''
html, used_encoding = decode_xml(data, normalize_to_nfc)
if used_encoding:
self.used_encoding = used_encoding return html def parse_xml(self, data):
data, self.used_encoding = xml_to_unicode( data, strip_encoding_pats=True, assume_utf8=True, resolve_entities=True)
data = unicodedata.normalize('NFC', data)
return safe_xml_fromstring(data)

def parse_xhtml(self, data, fname='<string>', force_html5_parse=False):
if self.tweak_mode:
return parse_html_tweak(data, log=self.log, decoder=self.decode, force_html5_parse=force_html5_parse)
else:
try:
return parse_html( data, log=self.log, decoder=self.decode, preprocessor=self.html_preprocessor, filename=fname, non_html_file_tags={'ncx'})
except NotHTML:
return self.parse_xml(data)

def parse_css(self, data, fname='<string>', is_declaration=False):
return parse_css(data, fname=fname, is_declaration=is_declaration, decode=self.decode, log_level=logging.WARNING, css_preprocessor=(None if self.tweak_mode else self.css_preprocessor))
# }}}

[문서]
class Container(ContainerBase): # {{{

'''
A container represents an open e-book as a folder full of files and an
OPF file. There are two important concepts:

* The root folder. This is the base of the e-book. All the e-books files are inside this folder or in its sub-folders.

* Names: These are paths to the books' files relative to the root folder. They always contain POSIX separators and are unquoted. They can be thought of as canonical identifiers for files in the book.
Most methods on the container object work with names. Names are always in the NFC Unicode normal form.

* Clones: the container object supports efficient on-disk cloning, which is used to implement checkpoints in the e-book editor. In order to make this work, you should never access files on the filesystem directly. Instead, use :meth:`raw_data` or
:meth:`open` to read/write to component files in the book.

When converting between hrefs and names use the methods provided by this class, they assume all hrefs are quoted.
'''

#: The type of book (epub for EPUB files and azw3 for AZW3 files)
book_type = 'oeb'
#: If this container represents an unzipped book (a directory)
is_dir = False

SUPPORTS_TITLEPAGES = True
SUPPORTS_FILENAMES = True
MAX_HTML_FILE_SIZE = 0

@property def book_type_for_display(self):
return self.book_type.upper()

def __init__(self, rootpath=None, opfpath=None, log=default_log, clone_data=None):
super().__init__(log=log)
self.root = clone_data['root'] if clone_data is not None else os.path.abspath(rootpath)

self.name_path_map = {}
self.dirtied = set()
self.pretty_print = set()
self.cloned = False self.cache_names = ('parsed_cache', 'mime_map', 'name_path_map', 'encoding_map', 'dirtied', 'pretty_print')
self.href_to_name_cache = {}

if clone_data is not None:
for x in ('cloned', 'name_path_map', 'opf_name', 'mime_map', 'pretty_print', 'encoding_map', 'tweak_mode'):
setattr(self, x, clone_data[x])
self.opf_dir = os.path.dirname(self.name_path_map[self.opf_name])
return

# Map of relative paths with '/' separators from root of unzipped ePub
# to absolute paths on filesystem with os-specific separators opfpath = os.path.abspath(os.path.realpath(opfpath))
all_opf_files = []
for dirpath, _dirnames, filenames in os.walk(self.root):
for f in filenames:
path = join(dirpath, f)
name = self.abspath_to_name(path)
self.name_path_map[name] = path self.mime_map[name] = guess_type(path)
# Special case if we have stumbled onto the opf if path == opfpath:
self.opf_name = name self.opf_dir = os.path.dirname(path)
self.mime_map[name] = guess_type('a.opf')
if path.lower().endswith('.opf'):
all_opf_files.append((name, os.path.dirname(path)))

if not hasattr(self, 'opf_name') and all_opf_files:
self.opf_name, self.opf_dir = all_opf_files[0]
self.mime_map[self.opf_name] = guess_type('a.opf')

if not hasattr(self, 'opf_name'):
raise InvalidBook(f'Could not locate opf file: {opfpath!r}')

# Update mime map with data from the OPF self.refresh_mime_map()

def refresh_mime_map(self):
for item in self.opf_xpath('//opf:manifest/opf:item[@href and @media-type]'):
href = item.get('href')
try:
name = self.href_to_name(href, self.opf_name)
except ValueError:
continue # special filenames such as CON on windows cause relpath to fail mt = item.get('media-type')
if name in self.mime_map and name != self.opf_name and mt:
# some epubs include the opf in the manifest with an incorrect mime type self.mime_map[name] = mt def data_for_clone(self, dest_dir=None):
dest_dir = dest_dir or self.root return {
'root': dest_dir,
'opf_name': self.opf_name,
'mime_map': self.mime_map.copy(),
'pretty_print': set(self.pretty_print),
'encoding_map': self.encoding_map.copy(),
'tweak_mode': self.tweak_mode,
'cloned': self.cloned,
'name_path_map': { name: os.path.join(dest_dir, os.path.relpath(path, self.root)) for name, path in self.name_path_map.items()
}
}

def clone_data(self, dest_dir):
Container.commit(self, keep_parsed=False)
clone_dir(self.root, dest_dir)
self.cloned = True return self.data_for_clone(dest_dir)

def __getstate__(self):
Container.commit(self, keep_parsed=True)
return self.data_for_clone()

def __setstate__(self, state):
self.__init__(log=default_log, clone_data=state)

[문서]
def add_name_to_manifest(self, name, process_manifest_item=None, suggested_id=''):
' Add an entry to the manifest for a file with the specified name. Returns the manifest id. '
all_ids = {x.get('id') for x in self.opf_xpath('//*[@id]')}
c = 0 item_id = suggested_id = suggested_id or 'id'
while item_id in all_ids:
c += 1 item_id = f'{suggested_id}-{c}'
manifest = self.opf_xpath('//opf:manifest')[0]
href = self.name_to_href(name, self.opf_name)
item = manifest.makeelement(OPF('item'), id=item_id, href=href)
item.set('media-type', self.mime_map[name])
self.insert_into_xml(manifest, item)
if process_manifest_item is not None:
process_manifest_item(item)
self.dirty(self.opf_name)
return item_id

[문서]
def manifest_has_name(self, name):
''' Return True if the manifest has an entry corresponding to name '''
all_names = {self.href_to_name(x.get('href'), self.opf_name) for x in self.opf_xpath('//opf:manifest/opf:item[@href]')}
return name in all_names

[문서]
def make_name_unique(self, name):
''' Ensure that `name` does not already exist in this book. If it does, return a modified version that does not exist. '''
counter = count()
while self.has_name_case_insensitive(name) or self.manifest_has_name(name):
c = next(counter) + 1 base, ext = name.rpartition('.')[::2]
if c > 1:
base = base.rpartition('-')[0]
name = f'{base}-{c}.{ext}'
return name

[문서]
def add_file( self, name, data=b'', media_type=None, spine_index=None, modify_name_if_needed=False, process_manifest_item=None, suggested_id='',
):
''' Add a file to this container. Entries for the file are automatically created in the OPF manifest and spine
(if the file is a text document) '''
if '..' in name:
raise ValueError('Names are not allowed to have .. in them')
href = self.name_to_href(name, self.opf_name)
if self.has_name_case_insensitive(name) or self.manifest_has_name(name):
if not modify_name_if_needed:
raise ValueError((f'A file with the name {name} already exists') if self.has_name_case_insensitive(name) else
(f'An item with the href {href} already exists in the manifest'))
name = self.make_name_unique(name)
href = self.name_to_href(name, self.opf_name)
path = self.name_to_abspath(name)
base = os.path.dirname(path)
if not os.path.exists(base):
os.makedirs(base)
with open(path, 'wb') as f:
if hasattr(data, 'read'):
shutil.copyfileobj(data, f)
else:
f.write(data)
mt = media_type or self.guess_type(name)
self.name_path_map[name] = path self.mime_map[name] = mt if self.ok_to_be_unmanifested(name):
return name item_id = self.add_name_to_manifest(name, process_manifest_item=process_manifest_item, suggested_id=suggested_id)
if mt in OEB_DOCS:
manifest = self.opf_xpath('//opf:manifest')[0]
spine = self.opf_xpath('//opf:spine')[0]
si = manifest.makeelement(OPF('itemref'), idref=item_id)
self.insert_into_xml(spine, si, index=spine_index)
return name[문서]
def rename(self, current_name, new_name):
''' Renames a file from current_name to new_name. It automatically rebases all links inside the file if the folder the file is in changes. Note however, that links are not updated in the other files that could reference this file. This is for performance, such updates should be done once, in bulk. '''
if current_name in self.names_that_must_not_be_changed:
raise ValueError(f'Renaming of {current_name} is not allowed')
if self.exists(new_name) and (new_name == current_name or new_name.lower() != current_name.lower()):
# The destination exists and does not differ from the current name only by case raise ValueError(f'Cannot rename {current_name} to {new_name} as {new_name} already exists')
new_path = self.name_to_abspath(new_name)
base = os.path.dirname(new_path)
if os.path.isfile(base):
raise ValueError(f'Cannot rename {current_name} to {new_name} as {base} is a file')
if not os.path.exists(base):
os.makedirs(base)
old_path = parent_dir = self.name_to_abspath(current_name)
self.commit_item(current_name)
os.rename(old_path, new_path)
# Remove empty directories while parent_dir:
parent_dir = os.path.dirname(parent_dir)
try:
os.rmdir(parent_dir)
except OSError:
break for x in ('mime_map', 'encoding_map'):
x = getattr(self, x)
if current_name in x:
x[new_name] = x[current_name]
self.name_path_map[new_name] = new_path for x in self.cache_names:
x = getattr(self, x)
try:
x.pop(current_name, None)
except TypeError:
x.discard(current_name)
if current_name == self.opf_name:
self.opf_name = new_name if os.path.dirname(old_path) != os.path.dirname(new_path):
from calibre.ebooks.oeb.polish.replace import LinkRebaser repl = LinkRebaser(self, current_name, new_name)
self.replace_links(new_name, repl)
self.dirty(new_name)

[문서]
def replace_links(self, name, replace_func):
''' Replace all links in name using replace_func, which must be a callable that accepts a URL and returns the replaced URL. It must also have a 'replaced' attribute that is set to True if any actual replacement is done. Convenient ways of creating such callables are using the :class:`LinkReplacer` and :class:`LinkRebaser` classes. '''
media_type = self.mime_map.get(name, guess_type(name))
if name == self.opf_name:
replace_func.file_type = 'opf'
for elem in self.opf_xpath('//*[@href]'):
elem.set('href', replace_func(elem.get('href')))
elif media_type.lower() in OEB_DOCS:
replace_func.file_type = 'text'
rewrite_links(self.parsed(name), replace_func)
elif media_type.lower() in OEB_STYLES:
replace_func.file_type = 'style'
replaceUrls(self.parsed(name), replace_func)
elif media_type.lower() == guess_type('toc.ncx'):
replace_func.file_type = 'ncx'
for elem in self.parsed(name).xpath('//*[@src]'):
elem.set('src', replace_func(elem.get('src')))

if replace_func.replaced:
self.dirty(name)
return replace_func.replaced

[문서]
def iterlinks(self, name, get_line_numbers=True):
''' Iterate over all links in name. If get_line_numbers is True the yields results of the form (link, line_number, offset). Where line_number is the line_number at which the link occurs and offset is the number of characters from the start of the line. Note that offset could actually encompass several lines if not zero. '''
media_type = self.mime_map.get(name, guess_type(name))
if name == self.opf_name:
for elem in self.opf_xpath('//*[@href]'):
yield (elem.get('href'), elem.sourceline, 0) if get_line_numbers else elem.get('href')
elif media_type.lower() in OEB_DOCS:
for el, attr, link, pos in iterlinks(self.parsed(name)):
yield (link, el.sourceline, pos) if get_line_numbers else link elif media_type.lower() in OEB_STYLES:
if get_line_numbers:
with self.open(name, 'rb') as f:
raw = self.decode(f.read()).replace('\r\n', '\n').replace('\r', '\n')
position = PositionFinder(raw)
is_in_comment = CommentFinder(raw)
for link, offset in itercsslinks(raw):
if not is_in_comment(offset):
lnum, col = position(offset)
yield link, lnum, col else:
for link in getUrls(self.parsed(name)):
yield link elif media_type.lower() == guess_type('toc.ncx'):
for elem in self.parsed(name).xpath('//*[@src]'):
yield (elem.get('src'), elem.sourceline, 0) if get_line_numbers else elem.get('src')

[문서]
def abspath_to_name(self, fullpath, root=None):
'''
Convert an absolute path to a canonical name relative to :attr:`root`

:param root: The base folder. By default the root for this container object is used.
'''
# OS X silently changes all file names to NFD form. The EPUB
# spec requires all text including filenames to be in NFC form.
# The proper fix is to implement a VFS that maps between
# canonical names and their file system representation, however,
# I don't have the time for that now. Note that the container
# ensures that all text files are normalized to NFC when
# decoding them anyway, so there should be no mismatch between
# names in the text and NFC canonical file names.
return unicodedata.normalize('NFC', abspath_to_name(fullpath, root or self.root))

[문서]
def name_to_abspath(self, name):
' Convert a canonical name to an absolute OS dependent path '
return name_to_abspath(name, self.root)

[문서]
def exists(self, name):
''' True iff a file/folder corresponding to the canonical name exists. Note that this function suffers from the limitations of the underlying OS filesystem, in particular case (in)sensitivity. So on a case insensitive filesystem this will return True even if the case of name is different from the case of the underlying filesystem file. See also :meth:`has_name`'''
return os.path.exists(self.name_to_abspath(name))

[문서]
def href_to_name(self, href, base=None):
'''
Convert an href (relative to base) to a name. base must be a name or
None, in which case self.root is used.
'''
key = href, base ans = self.href_to_name_cache.get(key, null)
if ans is null:
ans = self.href_to_name_cache[key] = href_to_name(href, self.root, base=base)
return ans

[문서]
def name_to_href(self, name, base=None):
'''Convert a name to a href relative to base, which must be a name or
None in which case self.root is used as the base'''
return name_to_href(name, self.root, base=base)

[문서]
def opf_xpath(self, expr):
' Convenience method to evaluate an XPath expression on the OPF file, has the opf: and dc: namespace prefixes pre-defined. '
return self.opf.xpath(expr, namespaces=OPF_NAMESPACES)

[문서]
def has_name(self, name):
''' Return True iff a file with the same canonical name as that specified exists. Unlike :meth:`exists` this method is always case-sensitive. '''
return name and name in self.name_path_map
def has_name_and_is_not_empty(self, name):
path = self.name_path_map.get(name)
if not path:
return False try:
if (sz := os.path.getsize(path)) == 0:
# this can happen when the directory entry is not flushed (which happens during fast EPUB extraction), so
# open the file and check to be sure.
with open(path) as f:
sz = f.seek(0, os.SEEK_END)
return sz > 0 except OSError:
return False def has_name_case_insensitive(self, name):
if not name:
return False name = name.lower()
for q in self.name_path_map:
if q.lower() == name:
return True return False

[문서]
def relpath(self, path, base=None):
'''Convert an absolute path (with os separators) to a path relative to base (defaults to self.root). The relative path is *not* a name. Use
:meth:`abspath_to_name` for that.'''
return relpath(path, base or self.root)

def ok_to_be_unmanifested(self, name):
return name in self.names_that_need_not_be_manifested

@property def names_that_need_not_be_manifested(self):
' Set of names that are allowed to be missing from the manifest. Depends on the e-book file format. '
return {self.opf_name}

@property def names_that_must_not_be_removed(self):
' Set of names that must never be deleted from the container. Depends on the e-book file format. '
return {self.opf_name}

@property def names_that_must_not_be_changed(self):
' Set of names that must never be renamed. Depends on the e-book file format. '
return set()

def parse(self, path, mime):
with open(path, 'rb') as src:
data = src.read()
if mime in OEB_DOCS:
data = self.parse_xhtml(data, self.relpath(path))
elif mime[-4:] in {'+xml', '/xml'}:
data = self.parse_xml(data)
elif mime in OEB_STYLES:
data = self.parse_css(data, self.relpath(path))
return data

[문서]
def raw_data(self, name, decode=True, normalize_to_nfc=True):
'''
Return the raw data corresponding to the file specified by name

:param decode: If True and the file has a text based MIME type, decode it and return a unicode object instead of raw bytes.
:param normalize_to_nfc: If True the returned unicode object is normalized to the NFC normal form as is required for the EPUB and AZW3 file formats.
'''
with self.open(name) as nf:
ans = nf.read()
mime = self.mime_map.get(name, guess_type(name))
if decode and (mime in OEB_STYLES or mime in OEB_DOCS or mime == 'text/plain' or mime[-4:] in {'+xml', '/xml'}):
ans = self.decode(ans, normalize_to_nfc=normalize_to_nfc)
return ans

[문서]
def parsed(self, name):
''' Return a parsed representation of the file specified by name. For
HTML and XML files an lxml tree is returned. For CSS files a css_parser stylesheet is returned. Note that parsed objects are cached for performance. If you make any changes to the parsed object, you must call :meth:`dirty` so that the container knows to update the cache. See also :meth:`replace`.'''
ans = self.parsed_cache.get(name, None)
if ans is None:
self.used_encoding = None mime = self.mime_map.get(name, guess_type(name))
ans = self.parse(self.name_path_map[name], mime)
self.parsed_cache[name] = ans self.encoding_map[name] = self.used_encoding return ans

[문서]
def replace(self, name, obj):
'''
Replace the parsed object corresponding to name with obj, which must be a similar object, i.e. an lxml tree for HTML/XML or a css_parser stylesheet for a CSS file.
'''
self.parsed_cache[name] = obj self.dirty(name)

@property def opf(self):
' The parsed OPF file '
return self.parsed(self.opf_name)

@property def mi(self):
''' The metadata of this book as a Metadata object. Note that this object is constructed on the fly every time this property is requested, so use it sparingly. '''
from calibre.ebooks.metadata.opf2 import OPF as O mi = self.serialize_item(self.opf_name)
return O(BytesIO(mi), basedir=self.opf_dir, unquote_urls=False, populate_spine=False).to_book_metadata()

@property def opf_version(self):
' The version set on the OPF\'s <package> element '
try:
return self.opf_xpath('//opf:package/@version')[0]
except IndexError:
return ''

@property def opf_version_parsed(self):
' The version set on the OPF\'s <package> element as a tuple of integers '
return parse_opf_version(self.opf_version)

@property def manifest_items(self):
return self.opf_xpath('//opf:manifest/opf:item[@href and @id]')

@property def manifest_id_map(self):
' Mapping of manifest id to canonical names '
return {item.get('id'):self.href_to_name(item.get('href'), self.opf_name) for item in self.manifest_items}

@property def manifest_type_map(self):
' Mapping of manifest media-type to list of canonical names of that media-type '
ans = defaultdict(list)
for item in self.opf_xpath('//opf:manifest/opf:item[@href and @media-type]'):
ans[item.get('media-type').lower()].append(self.href_to_name( item.get('href'), self.opf_name))
return {mt:tuple(v) for mt, v in ans.items()}

[문서]
def manifest_items_with_property(self, property_name):
' All manifest items that have the specified property '
prefixes = read_prefixes(self.opf)
for item in items_with_property(self.opf, property_name, prefixes):
href = item.get('href')
if href:
yield self.href_to_name(item.get('href'), self.opf_name)

[문서]
def manifest_items_of_type(self, predicate):
''' The names of all manifest items whose media-type matches predicate.
`predicate` can be a set, a list, a string or a function taking a single argument, which will be called with the media-type. '''
if isinstance(predicate, str):
predicate = predicate.__eq__ elif hasattr(predicate, '__contains__'):
predicate = predicate.__contains__ for mt, names in self.manifest_type_map.items():
if predicate(mt):
yield from names

[문서]
def apply_unique_properties(self, name, *properties):
''' Ensure that the specified properties are set on only the manifest item identified by name. You can pass None as the name to remove the property from all items. '''
properties = frozenset(properties)
removed_names, added_names = [], []
for p in properties:
if p.startswith('calibre:'):
ensure_prefix(self.opf, None, 'calibre', CALIBRE_PREFIX)
break for item in self.opf_xpath('//opf:manifest/opf:item'):
iname = self.href_to_name(item.get('href'), self.opf_name)
props = (item.get('properties') or '').split()
lprops = {p.lower() for p in props}
for prop in properties:
if prop.lower() in lprops:
if name != iname:
removed_names.append(iname)
props = [p for p in props if p.lower() != prop]
if props:
item.set('properties', ' '.join(props))
else:
del item.attrib['properties']
elif name == iname:
added_names.append(iname)
props.append(prop)
item.set('properties', ' '.join(props))
self.dirty(self.opf_name)
return removed_names, added_names

[문서]
def add_properties(self, name, *properties):
''' Add the specified properties to the manifest item identified by name. '''
properties = frozenset(properties)
if not properties:
return True for p in properties:
if p.startswith('calibre:'):
ensure_prefix(self.opf, None, 'calibre', CALIBRE_PREFIX)
break for item in self.opf_xpath('//opf:manifest/opf:item'):
iname = self.href_to_name(item.get('href'), self.opf_name)
if name == iname:
props = frozenset((item.get('properties') or '').split()) | properties item.set('properties', ' '.join(props))
return True return False

@property def guide_type_map(self):
' Mapping of guide type to canonical name '
return {item.get('type', ''):self.href_to_name(item.get('href'), self.opf_name)
for item in self.opf_xpath('//opf:guide/opf:reference[@href and @type]')}

@property def spine_iter(self):
''' An iterator that yields item, name is_linear for every item in the books' spine. item is the lxml element, name is the canonical file name and is_linear is True if the item is linear. See also: :attr:`spine_names` and :attr:`spine_items`. '''
manifest_id_map = self.manifest_id_map non_linear = []
for item in self.opf_xpath('//opf:spine/opf:itemref[@idref]'):
idref = item.get('idref')
name = manifest_id_map.get(idref, None)
path = self.name_path_map.get(name, None)
if path:
if item.get('linear', 'yes') == 'yes':
yield item, name, True else:
non_linear.append((item, name))
for item, name in non_linear:
yield item, name, False def index_in_spine(self, name):
manifest_id_map = self.manifest_id_map for i, item in enumerate(self.opf_xpath('//opf:spine/opf:itemref[@idref]')):
idref = item.get('idref')
q = manifest_id_map.get(idref, None)
if q == name:
return i

@property def spine_names(self):
''' An iterator yielding name and is_linear for every item in the books' spine. See also: :attr:`spine_iter` and :attr:`spine_items`. '''
for item, name, linear in self.spine_iter:
yield name, linear

@property def spine_items(self):
''' An iterator yielding the path for every item in the books' spine. See also: :attr:`spine_iter` and :attr:`spine_items`. '''
for name, linear in self.spine_names:
yield self.name_path_map[name]

[문서]
def remove_from_spine(self, spine_items, remove_if_no_longer_in_spine=True):
'''
Remove the specified items (by canonical name) from the spine. If ``remove_if_no_longer_in_spine`` is True, the items are also deleted from the book, not just from the spine.
'''
nixed = set()
for (name, remove), (item, xname, linear) in zip(spine_items, self.spine_iter):
if remove and name == xname:
self.remove_from_xml(item)
nixed.add(name)
if remove_if_no_longer_in_spine:
# Remove from the book if no longer in spine nixed -= {name for name, linear in self.spine_names}
for name in nixed:
self.remove_item(name)

[문서]
def set_spine(self, spine_items):
''' Set the spine to be spine_items where spine_items is an iterable of the form (name, linear). Will raise an error if one of the names is not present in the manifest. '''
imap = self.manifest_id_map imap = {name:item_id for item_id, name in imap.items()}
items = [item for item, name, linear in self.spine_iter]
tail, last_tail = (items[0].tail, items[-1].tail) if items else ('\n ', '\n ')
for i in items:
self.remove_from_xml(i)
spine = self.opf_xpath('//opf:spine')[0]
spine.text = tail for name, linear in spine_items:
i = spine.makeelement('{{{}}}itemref'.format(OPF_NAMESPACES['opf']), nsmap={'opf':OPF_NAMESPACES['opf']})
i.tail = tail i.set('idref', imap[name])
spine.append(i)
if not linear:
i.set('linear', 'no')
if len(spine) > 0:
spine[-1].tail = last_tail self.dirty(self.opf_name)[문서]
def remove_item(self, name, remove_from_guide=True):
'''
Remove the item identified by name from this container. This removes all references to the item in the OPF manifest, guide and spine as well as from any internal caches.
'''
removed = set()
for elem in self.opf_xpath('//opf:manifest/opf:item[@href]'):
if self.href_to_name(elem.get('href'), self.opf_name) == name:
id_ = elem.get('id', None)
if id_ is not None:
removed.add(id_)
self.remove_from_xml(elem)
self.dirty(self.opf_name)
if removed:
for spine in self.opf_xpath('//opf:spine'):
tocref = spine.attrib.get('toc', None)
if tocref and tocref in removed:
spine.attrib.pop('toc', None)
self.dirty(self.opf_name)

for item in self.opf_xpath('//opf:spine/opf:itemref[@idref]'):
idref = item.get('idref')
if idref in removed:
self.remove_from_xml(item)
self.dirty(self.opf_name)

for meta in self.opf_xpath('//opf:meta[@name="cover" and @content]'):
if meta.get('content') in removed:
self.remove_from_xml(meta)
self.dirty(self.opf_name)

for meta in self.opf_xpath('//opf:meta[@refines]'):
q = meta.get('refines')
if q.startswith('#') and q[1:] in removed:
self.remove_from_xml(meta)
self.dirty(self.opf_name)

if remove_from_guide:
for item in self.opf_xpath('//opf:guide/opf:reference[@href]'):
if self.href_to_name(item.get('href'), self.opf_name) == name:
self.remove_from_xml(item)
self.dirty(self.opf_name)

path = self.name_path_map.pop(name, None)
if path and os.path.exists(path):
os.remove(path)
self.mime_map.pop(name, None)
self.parsed_cache.pop(name, None)
self.dirtied.discard(name)def set_media_overlay_durations(self, duration_map=None):
self.dirty(self.opf_name)
for meta in self.opf_xpath('//opf:meta[@property="media:duration"]'):
self.remove_from_xml(meta)
metadata = self.opf_xpath('//opf:metadata')[0]
total_duration = 0 for item_id, duration in (duration_map or {}).items():
meta = metadata.makeelement(OPF('meta'), property='media:duration', refines='#' + item_id)
meta.text = seconds_to_timestamp(duration)
self.insert_into_xml(metadata, meta)
total_duration += duration if duration_map:
meta = metadata.makeelement(OPF('meta'), property='media:duration')
meta.text = seconds_to_timestamp(total_duration)
self.insert_into_xml(metadata, meta)

[문서]
def dirty(self, name):
''' Mark the parsed object corresponding to name as dirty. See also: :meth:`parsed`. '''
self.dirtied.add(name)

[문서]
def remove_from_xml(self, item):
'Removes item from parent, fixing indentation (works only with self closing items)'
parent = item.getparent()
idx = parent.index(item)
if idx == 0:
# We are removing the first item - only care about adjusting
# the tail if this was the only child if len(parent) == 1:
parent.text = item.tail else:
# Make sure the preceding item has this tail parent[idx-1].tail = item.tail parent.remove(item)
return item

[문서]
def insert_into_xml(self, parent, item, index=None):
'''Insert item into parent (or append if index is None), fixing indentation. Only works with self closing items.'''
insert_self_closing(parent, item, index)

[문서]
def opf_get_or_create(self, name):
''' Convenience method to either return the first XML element with the specified name or create it under the opf:package element and then return it, if it does not already exist. '''
ans = self.opf_xpath('//opf:'+name)
if ans:
return ans[0]
self.dirty(self.opf_name)
package = self.opf_xpath('//opf:package')[0]
item = package.makeelement(OPF(name))
item.tail = '\n'
package.append(item)
return item

[문서]
def generate_item(self, name, id_prefix=None, media_type=None, unique_href=True):
'''Add an item to the manifest with href derived from the given name. Ensures uniqueness of href and id automatically. Returns generated item.'''
id_prefix = id_prefix or 'id'
media_type = media_type or self.guess_type(name)
if unique_href:
name = self.make_name_unique(name)
href = self.name_to_href(name, self.opf_name)
base, ext = href.rpartition('.')[0::2]
all_ids = {x.get('id') for x in self.opf_xpath('//*[@id]')}
if id_prefix.endswith('-'):
all_ids.add(id_prefix)
c = 0 item_id = id_prefix while item_id in all_ids:
c += 1 item_id = f'{id_prefix}{c}'

manifest = self.opf_xpath('//opf:manifest')[0]
item = manifest.makeelement(OPF('item'), id=item_id, href=href)
item.set('media-type', media_type)
self.insert_into_xml(manifest, item)
self.dirty(self.opf_name)
name = self.href_to_name(href, self.opf_name)
self.name_path_map[name] = path = self.name_to_abspath(name)
self.mime_map[name] = media_type
# Ensure that the file corresponding to the newly created item exists
# otherwise cloned containers will fail when they try to get the number
# of links to the file base = os.path.dirname(path)
if not os.path.exists(base):
os.makedirs(base)
open(path, 'wb').close()
return itemdef format_opf(self):
try:
mdata = self.opf_xpath('//opf:metadata')[0]
except IndexError:
pass else:
mdata.text = '\n '
remove = set()
for child in mdata:
child.tail = '\n '
try:
if (child.get('name', '').startswith('calibre:'
) and child.get('content', '').strip() in {'{}', ''}):
remove.add(child)
except AttributeError:
continue # Happens for XML comments for child in remove:
mdata.remove(child)
if len(mdata) > 0:
mdata[-1].tail = '\n '
# Ensure name comes before content, needed for Nooks for meta in self.opf_xpath('//opf:meta[@name="cover"]'):
if 'content' in meta.attrib:
meta.set('content', meta.attrib.pop('content'))

[문서]
def serialize_item(self, name):
''' Convert a parsed object (identified by canonical name) into a bytestring. See :meth:`parsed`. '''
data = root = self.parsed(name)
if name == self.opf_name:
self.format_opf()
data = serialize(data, self.mime_map[name], pretty_print=name in self.pretty_print)
if name == self.opf_name and root.nsmap.get(None) == OPF2_NS:
# Needed as I can't get lxml to output opf:role and
# not output <opf:metadata> as well data = re.sub(br'(<[/]{0,1})opf:', br'\1', data)
return data

[문서]
def commit_item(self, name, keep_parsed=False):
''' Commit a parsed object to disk (it is serialized and written to the underlying file). If ``keep_parsed`` is True the parsed representation is retained in the cache. See also: :meth:`parsed` '''
if name not in self.parsed_cache:
return data = self.serialize_item(name)
self.dirtied.discard(name)
if not keep_parsed:
self.parsed_cache.pop(name)
dest = self.name_path_map[name]
if self.cloned and nlinks_file(dest) > 1:
# Decouple this file from its links os.unlink(dest)
with open(dest, 'wb') as f:
f.write(data)

[문서]
def filesize(self, name):
''' Return the size in bytes of the file represented by the specified canonical name. Automatically handles dirtied parsed objects. See also:
:meth:`parsed` '''
if name in self.dirtied:
self.commit_item(name, keep_parsed=True)
path = self.name_to_abspath(name)
return os.path.getsize(path)

[문서]
def get_file_path_for_processing(self, name, allow_modification=True):
''' Similar to open() except that it returns a file path, instead of an open file object. '''
if name in self.dirtied:
self.commit_item(name)
self.parsed_cache.pop(name, False)
path = self.name_to_abspath(name)
base = os.path.dirname(path)
if not os.path.exists(base):
os.makedirs(base)
elif self.cloned and allow_modification and os.path.exists(path) and nlinks_file(path) > 1:
# Decouple this file from its links temp = path + 'xxx'
shutil.copyfile(path, temp)
if iswindows:
retry_on_fail(os.unlink, path)
else:
os.unlink(path)
os.rename(temp, path)
return path

[문서]
def open(self, name, mode='rb'):
''' Open the file pointed to by name for direct read/write. Note that this will commit the file if it is dirtied and remove it from the parse cache. You must finish with this file before accessing the parsed version of it again, or bad things will happen. '''
return open(make_long_path_useable(self.get_file_path_for_processing(name, mode not in {'r', 'rb'})), mode)

[문서]
def commit(self, outpath=None, keep_parsed=False):
'''
Commit all dirtied parsed objects to the filesystem and write out the e-book file at outpath.

:param output: The path to write the saved e-book file to. If None, the path of the original book file is used.
:param keep_parsed: If True the parsed representations of committed items are kept in the cache.
'''
for name in tuple(self.dirtied):
self.commit_item(name, keep_parsed=keep_parsed)

def compare_to(self, other):
if set(self.name_path_map) != set(other.name_path_map):
return 'Set of files is not the same'
mismatches = []
for name, path in self.name_path_map.items():
opath = other.name_path_map[name]
with open(path, 'rb') as f1, open(opath, 'rb') as f2:
if f1.read() != f2.read():
mismatches.append(f'The file {name} is not the same')
return '\n'.join(mismatches)# }}}

# EPUB {{{ class InvalidEpub(InvalidBook):
pass class ObfuscationKeyMissing(InvalidEpub):
pass

OCF_NS = 'urn:oasis:names:tc:opendocument:xmlns:container'
VCS_IGNORE_FILES = frozenset('.gitignore .hgignore .agignore .bzrignore'.split())
VCS_DIRS = frozenset(('.git', '.hg', '.svn', '.bzr'))

def walk_dir(basedir):
for dirpath, dirnames, filenames in os.walk(basedir):
for vcsdir in VCS_DIRS:
try:
dirnames.remove(vcsdir)
except Exception:
pass is_root = os.path.abspath(os.path.normcase(dirpath)) == os.path.abspath(os.path.normcase(basedir))
yield is_root, dirpath, None for fname in filenames:
if fname not in VCS_IGNORE_FILES:
yield is_root, dirpath, fname class EpubContainer(Container):

book_type = 'epub'
MAX_HTML_FILE_SIZE = 260 * 1024

@property def book_type_for_display(self):
ans = self.book_type.upper()
try:
v = self.opf_version_parsed except Exception:
pass else:
try:
if v.major == 2:
ans += ' 2'
elif not v.minor:
ans += f' {v.major}'
else:
ans += f' {v.major}.{v.minor}'
except Exception:
pass return ans

META_INF = {
'container.xml': True,
'manifest.xml': False,
'encryption.xml': False,
'metadata.xml': False,
'signatures.xml': False,
'rights.xml': False,
}

def __init__(self, pathtoepub=None, log=default_log, clone_data=None, tdir=None):
if clone_data is not None:
super().__init__(log=log, clone_data=clone_data)
for x in ('pathtoepub', 'obfuscated_fonts', 'is_dir'):
setattr(self, x, clone_data[x])
return self.pathtoepub = pathtoepub if tdir is None:
tdir = PersistentTemporaryDirectory('_epub_container')
tdir = os.path.abspath(os.path.realpath(tdir))
self.root = tdir self.is_dir = os.path.isdir(pathtoepub)
if self.is_dir:
for is_root, dirpath, fname in walk_dir(self.pathtoepub):
if is_root:
base = tdir else:
base = os.path.join(tdir, os.path.relpath(dirpath, self.pathtoepub))
if fname is None:
os.mkdir(base)
if fname is not None:
shutil.copy(os.path.join(dirpath, fname), os.path.join(base, fname))
else:
with open(self.pathtoepub, 'rb') as stream:
try:
zf = ZipFile(stream)
zf.extractall(tdir)
except Exception:
if log is not None:
log.exception('EPUB appears to be invalid ZIP file, trying a more forgiving ZIP parser')
from calibre.utils.localunzip import extractall stream.seek(0)
extractall(stream, path=tdir)
try:
os.remove(join(tdir, 'mimetype'))
except OSError:
pass
# Ensure all filenames are in NFC normalized form
# has no effect on HFS+ filesystems as they always store filenames
# in NFD form for filename in walk(self.root):
n = unicodedata.normalize('NFC', filename)
if n != filename:
s = filename + 'suff1x'
os.rename(filename, s)
os.rename(s, n)

container_path = join(self.root, 'META-INF', 'container.xml')
if not exists(container_path):
raise InvalidEpub('No META-INF/container.xml in epub')
with open(container_path, 'rb') as cf:
container = safe_xml_fromstring(cf.read())
opf_files = container.xpath(( r'child::ocf:rootfiles/ocf:rootfile'
'[@media-type="{}" and @full-path]'.format(guess_type('a.opf'))
), namespaces={'ocf':OCF_NS}
)
if not opf_files:
raise InvalidEpub('META-INF/container.xml contains no link to OPF file')
opf_path = os.path.join(self.root, *(urlunquote(opf_files[0].get('full-path')).split('/')))
if not exists(opf_path):
raise InvalidEpub('OPF file does not exist at location pointed to'
' by META-INF/container.xml')

super().__init__(rootpath=tdir, opfpath=opf_path, log=log)

self.obfuscated_fonts = {}
if 'META-INF/encryption.xml' in self.name_path_map:
self.process_encryption()
self.parsed_cache['META-INF/container.xml'] = container def data_for_clone(self, dest_dir=None):
ans = super().data_for_clone(dest_dir)
ans['pathtoepub'] = self.pathtoepub ans['obfuscated_fonts'] = self.obfuscated_fonts.copy()
ans['is_dir'] = self.is_dir return ans def rename(self, old_name, new_name):
is_opf = old_name == self.opf_name super().rename(old_name, new_name)
if is_opf:
for elem in self.parsed('META-INF/container.xml').xpath(( r'child::ocf:rootfiles/ocf:rootfile'
'[@media-type="{}" and @full-path]'.format(guess_type('a.opf'))
), namespaces={'ocf':OCF_NS}
):
# The asinine epubcheck cannot handle quoted filenames in
# container.xml elem.set('full-path', self.opf_name)
self.dirty('META-INF/container.xml')
if old_name in self.obfuscated_fonts:
self.obfuscated_fonts[new_name] = self.obfuscated_fonts.pop(old_name)
enc = self.parsed('META-INF/encryption.xml')
for cr in enc.xpath('//*[local-name()="CipherReference" and @URI]'):
if self.href_to_name(cr.get('URI')) == old_name:
cr.set('URI', self.name_to_href(new_name))
self.dirty('META-INF/encryption.xml')

@property def names_that_need_not_be_manifested(self):
return super().names_that_need_not_be_manifested | {'META-INF/' + x for x in self.META_INF}

def ok_to_be_unmanifested(self, name):
return name in self.names_that_need_not_be_manifested or name.startswith('META-INF/')

@property def names_that_must_not_be_removed(self):
return super().names_that_must_not_be_removed | {'META-INF/container.xml'}

@property def names_that_must_not_be_changed(self):
return super().names_that_must_not_be_changed | {'META-INF/' + x for x in self.META_INF}

def remove_item(self, name, remove_from_guide=True):
# Handle removal of obfuscated fonts if name == 'META-INF/encryption.xml':
self.obfuscated_fonts.clear()
if name in self.obfuscated_fonts:
self.obfuscated_fonts.pop(name, None)
enc = self.parsed('META-INF/encryption.xml')
for em in enc.xpath('//*[local-name()="EncryptionMethod" and @Algorithm]'):
alg = em.get('Algorithm')
if alg not in {ADOBE_OBFUSCATION, IDPF_OBFUSCATION}:
continue try:
cr = em.getparent().xpath('descendant::*[local-name()="CipherReference" and @URI]')[0]
except (IndexError, ValueError, KeyError):
continue if name == self.href_to_name(cr.get('URI')):
self.remove_from_xml(em.getparent())
self.dirty('META-INF/encryption.xml')
super().remove_item(name, remove_from_guide=remove_from_guide)

def read_raw_unique_identifier(self):
package_id = raw_unique_identifier = idpf_key = None for attrib, val in self.opf.attrib.items():
if attrib.endswith('unique-identifier'):
package_id = val break if package_id is not None:
for elem in self.opf_xpath(f'//*[@id={escape_xpath_attr(package_id)}]'):
if elem.text:
raw_unique_identifier = elem.text break if raw_unique_identifier is not None:
idpf_key = raw_unique_identifier idpf_key = re.sub(r'[ \t\r\n]', '', idpf_key)
idpf_key = hashlib.sha1(idpf_key.encode('utf-8')).digest()
return package_id, raw_unique_identifier, idpf_key def iter_encryption_entries(self):
if 'META-INF/encryption.xml' in self.name_path_map:
enc = self.parsed('META-INF/encryption.xml')
for em in enc.xpath('//*[local-name()="EncryptionMethod" and @Algorithm]'):
try:
cr = em.getparent().xpath('descendant::*[local-name()="CipherReference" and @URI]')[0]
except Exception:
cr = None yield em, cr def process_encryption(self):
fonts = {}
for em, cr in self.iter_encryption_entries():
alg = em.get('Algorithm')
if alg not in {ADOBE_OBFUSCATION, IDPF_OBFUSCATION}:
raise DRMError()
if cr is None:
continue name = self.href_to_name(cr.get('URI'))
path = self.name_path_map.get(name, None)
if path is not None:
fonts[name] = alg package_id, raw_unique_identifier, idpf_key = self.read_raw_unique_identifier()
key = None for item in self.opf_xpath('//*[local-name()="metadata"]/*'
'[local-name()="identifier"]'):
scheme = None for xkey in item.attrib.keys():
if xkey.endswith('scheme'):
scheme = item.get(xkey)
if (scheme and scheme.lower() == 'uuid') or \ (item.text and item.text.startswith('urn:uuid:')):
try:
key = item.text.rpartition(':')[-1]
key = uuid.UUID(key).bytes except Exception:
if self.log is not None:
self.log.exception('Failed to parse obfuscation key')
key = None for font, alg in fonts.items():
tkey = key if alg == ADOBE_OBFUSCATION else idpf_key if not tkey:
raise ObfuscationKeyMissing('Failed to find obfuscation key')
raw = self.raw_data(font, decode=False)
raw = decrypt_font_data(tkey, raw, alg)
with self.open(font, 'wb') as f:
f.write(raw)
self.obfuscated_fonts[font] = (alg, tkey)

def update_modified_timestamp(self):
from calibre.ebooks.metadata.opf3 import set_last_modified_in_opf set_last_modified_in_opf(self.opf)
self.dirty(self.opf_name)

def commit(self, outpath=None, keep_parsed=False):
if self.opf_version_parsed.major == 3:
self.update_modified_timestamp()
super().commit(keep_parsed=keep_parsed)
container_path = join(self.root, 'META-INF', 'container.xml')
if not exists(container_path):
raise InvalidEpub('No META-INF/container.xml in EPUB, this typically happens if the temporary files calibre'
' is using are deleted by some other program while calibre is running')
restore_fonts = {}
for name in self.obfuscated_fonts:
if name not in self.name_path_map:
continue alg, key = self.obfuscated_fonts[name]
# Decrypting and encrypting are the same operation (XOR with key)
restore_fonts[name] = data = self.raw_data(name, decode=False)
with self.open(name, 'wb') as f:
f.write(decrypt_font_data(key, data, alg))
if outpath is None:
outpath = self.pathtoepub self.commit_epub(outpath)
for name, data in restore_fonts.items():
with self.open(name, 'wb') as f:
f.write(data)

def commit_epub(self, outpath: str) -> None:
if self.is_dir:
# First remove items from the source dir that do not exist any more for is_root, dirpath, fname in walk_dir(self.pathtoepub):
if fname is not None:
if is_root and fname == 'mimetype':
continue base = self.root if is_root else os.path.join(self.root, os.path.relpath(dirpath, self.pathtoepub))
fpath = os.path.join(base, fname)
if not os.path.exists(fpath):
os.remove(os.path.join(dirpath, fname))
try:
os.rmdir(dirpath)
except OSError as err:
if err.errno != errno.ENOTEMPTY:
raise
# Now copy over everything from root to source dir for dirpath, dirnames, filenames in os.walk(self.root):
is_root = os.path.abspath(os.path.normcase(dirpath)) == os.path.abspath(os.path.normcase(self.root))
base = self.pathtoepub if is_root else os.path.join(self.pathtoepub, os.path.relpath(dirpath, self.root))
try:
os.mkdir(base)
except OSError as err:
if err.errno != errno.EEXIST:
raise for fname in filenames:
with open(os.path.join(dirpath, fname), 'rb') as src, open(os.path.join(base, fname), 'wb') as dest:
shutil.copyfileobj(src, dest)

else:
from calibre.ebooks.tweak import zip_rebuilder with open(join(self.root, 'mimetype'), 'wb') as f:
et = guess_type('a.epub')
if not isinstance(et, bytes):
et = et.encode('ascii')
f.write(et)
zip_rebuilder(self.root, outpath)

@property def path_to_ebook(self):
return self.pathtoepub

@path_to_ebook.setter def path_to_ebook(self, val):
self.pathtoepub = val

# }}}

class KEPUBContainer(EpubContainer):
book_type = 'kepub'
MAX_HTML_FILE_SIZE = 512 * 1024 def __init__(self, pathtokepub=None, log=default_log, clone_data=None, tdir=None):
super().__init__(pathtokepub, log=log, clone_data=clone_data, tdir=tdir)
from calibre.ebooks.oeb.polish.kepubify import unkepubify_container
Container.commit(self, keep_parsed=True)
unkepubify_container(self)

def commit_epub(self, outpath: str) -> None:
if self.is_dir:
return super().commit_epub(outpath)
from calibre.ebooks.oeb.polish.kepubify import Options, kepubify_container with TemporaryDirectory() as tdir:
container = clone_container(self, tdir, container_class=EpubContainer)
kepubify_container(container, Options())
container.commit(outpath)

# AZW3 {{{ class InvalidMobi(InvalidBook):
pass def do_explode(path, dest):
from calibre.ebooks.mobi.reader.mobi6 import MobiReader from calibre.ebooks.mobi.reader.mobi8 import Mobi8Reader with open(path, 'rb') as stream:
mr = MobiReader(stream, default_log, None, None)

with CurrentDir(dest):
mr = Mobi8Reader(mr, default_log, for_tweak=True)
opf = os.path.abspath(mr())
obfuscated_fonts = mr.encrypted_fonts return opf, obfuscated_fonts def opf_to_azw3(opf, outpath, container):
from calibre.ebooks.conversion.plumber import Plumber, create_oebbook from calibre.ebooks.mobi.tweak import set_cover class Item(Manifest.Item):

def _parse_css(self, data):
# The default CSS parser used by oeb.base inserts the h namespace
# and resolves all @import rules. We don't want that.
return container.parse_css(data)

def specialize(oeb):
oeb.manifest.Item = Item plumber = Plumber(opf, outpath, container.log)
plumber.setup_options()
inp = plugin_for_input_format('azw3')
outp = plugin_for_output_format('azw3')
plumber.opts.mobi_passthrough = True plumber.opts.keep_ligatures = True oeb = create_oebbook(container.log, opf, plumber.opts, specialize=specialize)
set_cover(oeb)
outp.convert(oeb, outpath, inp, plumber.opts, container.log)

def epub_to_azw3(epub, outpath=None):
container = get_container(epub, tweak_mode=True)
changed = False for item in container.opf_xpath('//opf:manifest/opf:item[@properties and @href]'):
p = item.get('properties').split()
if 'cover-image' in p:
href = item.get('href')
guides = container.opf_xpath('//opf:guide')
if not guides:
guides = (container.opf.makeelement(OPF('guide')),)
container.opf.append(guides[0])
for guide in guides:
for child in guide:
if child.get('type') == 'cover':
break else:
guide.append(guide.makeelement(OPF('reference'), type='cover', href=href))
changed = True break elif 'calibre:title-page' in p:
item.getparent().remove(item)
if changed:
container.dirty(container.opf_name)
container.commit_item(container.opf_name)
outpath = outpath or (epub.rpartition('.')[0] + '.azw3')
opf_to_azw3(container.name_to_abspath(container.opf_name), outpath, container)

class AZW3Container(Container):

book_type = 'azw3'
SUPPORTS_TITLEPAGES = False
SUPPORTS_FILENAMES = False def __init__(self, pathtoazw3=None, log=default_log, clone_data=None, tdir=None):
if clone_data is not None:
super().__init__(log=log, clone_data=clone_data)
for x in ('pathtoazw3', 'obfuscated_fonts'):
setattr(self, x, clone_data[x])
return self.pathtoazw3 = pathtoazw3 if tdir is None:
tdir = PersistentTemporaryDirectory('_azw3_container')
tdir = os.path.abspath(os.path.realpath(tdir))
self.root = tdir with open(pathtoazw3, 'rb') as stream:
raw = stream.read(3)
if raw == b'TPZ':
raise InvalidMobi(_('This is not a MOBI file. It is a Topaz file.'))

try:
header = MetadataHeader(stream, default_log)
except MobiError:
raise InvalidMobi(_('This is not a MOBI file.'))

if header.encryption_type != 0:
raise DRMError()

kf8_type = header.kf8_type if kf8_type is None:
raise InvalidMobi(_('This MOBI file does not contain a KF8 format '
'book. KF8 is the new format from Amazon. calibre can '
'only edit MOBI files that contain KF8 books. Older '
'MOBI files without KF8 are not editable.'))

if kf8_type == 'joint':
raise InvalidMobi(_('This MOBI file contains both KF8 and '
'older Mobi6 data. calibre can only edit MOBI files '
'that contain only KF8 data.'))

try:
opf_path, obfuscated_fonts = fork_job(
'calibre.ebooks.oeb.polish.container', 'do_explode', args=(pathtoazw3, tdir), no_output=True)['result']
except WorkerError as e:
if log is not None:
log(e.orig_tb)
raise InvalidMobi('Failed to explode MOBI')
super().__init__(rootpath=tdir, opfpath=opf_path, log=log)
self.obfuscated_fonts = {x.replace(os.sep, '/') for x in obfuscated_fonts}

def data_for_clone(self, dest_dir=None):
ans = super().data_for_clone(dest_dir)
ans['pathtoazw3'] = self.pathtoazw3 ans['obfuscated_fonts'] = self.obfuscated_fonts.copy()
return ans def commit(self, outpath=None, keep_parsed=False):
super().commit(keep_parsed=keep_parsed)
if outpath is None:
outpath = self.pathtoazw3 opf_to_azw3(self.name_path_map[self.opf_name], outpath, self)

@property def path_to_ebook(self):
return self.pathtoazw3

@path_to_ebook.setter def path_to_ebook(self, val):
self.pathtoazw3 = val

@property def names_that_must_not_be_changed(self):
return set(self.name_path_map)
# }}}

def get_container(path, log=None, tdir=None, tweak_mode=False, ebook_cls=None) -> Container:
try:
isdir = os.path.isdir(path)
except Exception:
isdir = False own_tdir = not tdir if ebook_cls is None:
ext = path.rpartition('.')[-1].lower()
ebook_cls = EpubContainer if not isdir:
if ext in {'azw3', 'mobi', 'original_azw3', 'original_mobi'}:
ebook_cls = AZW3Container elif ext in {'kepub', 'original_kepub'}:
ebook_cls = KEPUBContainer if own_tdir:
tdir = PersistentTemporaryDirectory(f'_{ebook_cls.book_type}_container')
try:
ebook = ebook_cls(path, log=log or default_log, tdir=tdir)
ebook.tweak_mode = tweak_mode except BaseException:
if own_tdir:
shutil.rmtree(tdir, ignore_errors=True)
raise return ebook def test_roundtrip():
ebook = get_container(sys.argv[-1])
p = PersistentTemporaryFile(suffix='.'+sys.argv[-1].rpartition('.')[-1])
p.close()
ebook.commit(outpath=p.name)
ebook2 = get_container(p.name)
ebook3 = get_container(p.name)
diff = ebook3.compare_to(ebook2)
if diff is not None:
print(diff)

if __name__ == '__main__':
test_roundtrip()

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 시작 »
- 모듈 코드 »
- calibre.ebooks.oeb.polish.cover

calibre.ebooks.oeb.polish.cover의 소스 코드

__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import os import re import shutil from calibre.ebooks.oeb.base import OEB_DOCS, OPF, XLINK, XPath, xml2text from calibre.ebooks.oeb.polish.replace import get_recommended_folders, replace_links from calibre.utils.imghdr import identify def set_azw3_cover(container, cover_path, report, options=None):
existing_image = options is not None and options.get('existing_image', False)
name = None found = True for gi in container.opf_xpath('//opf:guide/opf:reference[@href and contains(@type, "cover")]'):
href = gi.get('href')
name = container.href_to_name(href, container.opf_name)
container.remove_from_xml(gi)
if existing_image:
name = cover_path found = False elif name is None or not container.has_name(name):
item = container.generate_item(name='cover.jpeg', id_prefix='cover')
name = container.href_to_name(item.get('href'), container.opf_name)
found = False href = container.name_to_href(name, container.opf_name)
guide = container.opf_xpath('//opf:guide')[0]
container.insert_into_xml(guide, guide.makeelement(
OPF('reference'), href=href, type='cover'))
if not existing_image:
with open(cover_path, 'rb') as src, container.open(name, 'wb') as dest:
shutil.copyfileobj(src, dest)
container.dirty(container.opf_name)
report(_('Cover updated') if found else _('Cover inserted'))

def get_azw3_raster_cover_name(container):
items = container.opf_xpath('//opf:guide/opf:reference[@href and contains(@type, "cover")]')
if items:
return container.href_to_name(items[0].get('href'))

def mark_as_cover_azw3(container, name):
href = container.name_to_href(name, container.opf_name)
found = False for item in container.opf_xpath('//opf:guide/opf:reference[@href and contains(@type, "cover")]'):
item.set('href', href)
found = True if not found:
for guide in container.opf_xpath('//opf:guide'):
container.insert_into_xml(guide, guide.makeelement(
OPF('reference'), href=href, type='cover'))
container.dirty(container.opf_name)

def get_raster_cover_name(container):
if container.book_type == 'azw3':
return get_azw3_raster_cover_name(container)
return find_cover_image(container, strict=True)

def get_cover_page_name(container):
if container.book_type == 'azw3':
return return find_cover_page(container)

[문서]
def set_cover(container, cover_path, report=None, options=None):
'''
Set the cover of the book to the image pointed to by cover_path.

:param cover_path: Either the absolute path to an image file or the canonical name of an image in the book. When using an image in the book, you must also set options, see below.
:param report: An optional callable that takes a single argument. It will be called with information about the tasks being processed.
:param options: None or a dictionary that controls how the cover is set. The dictionary can have entries:
**keep_aspect**: True or False (Preserve aspect ratio of covers in EPUB)
**no_svg**: True or False (Use an SVG cover wrapper in the EPUB titlepage)
**existing**: True or False (``cover_path`` refers to an existing image in the book)
'''
report = report or (lambda x:x)
if container.book_type == 'azw3':
set_azw3_cover(container, cover_path, report, options=options)
else:
set_epub_cover(container, cover_path, report, options=options)

[문서]
def mark_as_cover(container, name):
'''
Mark the specified image as the cover image.
'''
if name not in container.mime_map:
raise ValueError(f'Cannot mark {name} as cover as it does not exist')
mt = container.mime_map[name]
if not is_raster_image(mt):
raise ValueError(f'Cannot mark {name} as the cover image as it is not a raster image')
if container.book_type == 'azw3':
mark_as_cover_azw3(container, name)
else:
mark_as_cover_epub(container, name)

###############################################################################
# The delightful EPUB cover processing def is_raster_image(media_type):
return media_type and media_type.lower() in {
'image/png', 'image/jpeg', 'image/jpg', 'image/gif'}

COVER_TYPES = {
'coverimagestandard', 'other.ms-coverimage-standard',
'other.ms-titleimage-standard', 'other.ms-titleimage',
'other.ms-coverimage', 'other.ms-thumbimage-standard',
'other.ms-thumbimage', 'thumbimagestandard', 'cover'}

def find_cover_image2(container, strict=False):
manifest_id_map = container.manifest_id_map mm = container.mime_map for meta in container.opf_xpath('//opf:meta[@name="cover" and @content]'):
item_id = meta.get('content')
name = manifest_id_map.get(item_id, None)
media_type = mm.get(name, None)
if is_raster_image(media_type):
return name

# First look for a guide item with type == 'cover'
guide_type_map = container.guide_type_map for ref_type, name in guide_type_map.items():
if ref_type.lower() == 'cover' and is_raster_image(mm.get(name, None)):
return name if strict:
return

# Find the largest image from all possible guide cover items largest_cover = (None, 0)
for ref_type, name in guide_type_map.items():
if ref_type.lower() in COVER_TYPES and is_raster_image(mm.get(name, None)):
path = container.name_path_map.get(name, None)
if path:
sz = os.path.getsize(path)
if sz > largest_cover[1]:
largest_cover = (name, sz)

if largest_cover[0]:
return largest_cover[0]

def find_cover_image3(container):
for name in container.manifest_items_with_property('cover-image'):
return name manifest_id_map = container.manifest_id_map mm = container.mime_map for meta in container.opf_xpath('//opf:meta[@name="cover" and @content]'):
item_id = meta.get('content')
name = manifest_id_map.get(item_id, None)
media_type = mm.get(name, None)
if is_raster_image(media_type):
return name def find_cover_image(container, strict=False):
'Find a raster image marked as a cover in the OPF'
ver = container.opf_version_parsed if ver.major < 3:
return find_cover_image2(container, strict=strict)
else:
return find_cover_image3(container)

def get_guides(container):
guides = container.opf_xpath('//opf:guide')
if not guides:
container.insert_into_xml(container.opf, container.opf.makeelement(
OPF('guide')))
guides = container.opf_xpath('//opf:guide')
return guides def mark_as_cover_epub(container, name):
mmap = {v:k for k, v in container.manifest_id_map.items()}
if name not in mmap:
raise ValueError(f'Cannot mark {name} as cover as it is not in manifest')
mid = mmap[name]
ver = container.opf_version_parsed

# Remove all entries from the opf that identify a raster image as cover for meta in container.opf_xpath('//opf:meta[@name="cover" and @content]'):
container.remove_from_xml(meta)
for ref in container.opf_xpath('//opf:guide/opf:reference[@href and @type]'):
if ref.get('type').lower() not in COVER_TYPES:
continue rname = container.href_to_name(ref.get('href'), container.opf_name)
mt = container.mime_map.get(rname, None)
if is_raster_image(mt):
container.remove_from_xml(ref)

if ver.major < 3:
# Add reference to image in <metadata> for metadata in container.opf_xpath('//opf:metadata'):
m = metadata.makeelement(OPF('meta'), name='cover', content=mid)
container.insert_into_xml(metadata, m)

# If no entry for cover exists in guide, insert one that points to this
# image if not container.opf_xpath('//opf:guide/opf:reference[@type="cover"]'):
for guide in get_guides(container):
container.insert_into_xml(guide, guide.makeelement(
OPF('reference'), type='cover', href=container.name_to_href(name, container.opf_name)))
else:
container.apply_unique_properties(name, 'cover-image')

container.dirty(container.opf_name)

[문서]
def mark_as_titlepage(container, name, move_to_start=True):
'''
Mark the specified HTML file as the titlepage of the EPUB.

:param move_to_start: If True the HTML file is moved to the start of the spine
'''
ver = container.opf_version_parsed if move_to_start:
for item, q, linear in container.spine_iter:
if name == q:
break if not linear:
item.set('linear', 'yes')
if item.getparent().index(item) > 0:
container.insert_into_xml(item.getparent(), item, 0)
if ver.major < 3:
for ref in container.opf_xpath('//opf:guide/opf:reference[@type="cover"]'):
ref.getparent().remove(ref)

for guide in get_guides(container):
container.insert_into_xml(guide, guide.makeelement(
OPF('reference'), type='cover', href=container.name_to_href(name, container.opf_name)))
else:
container.apply_unique_properties(name, 'calibre:title-page')

container.dirty(container.opf_name)

def find_cover_page(container):
'Find a document marked as a cover in the OPF'
ver = container.opf_version_parsed mm = container.mime_map if ver.major < 3:
guide_type_map = container.guide_type_map for ref_type, name in guide_type_map.items():
if ref_type.lower() == 'cover' and mm.get(name, '').lower() in OEB_DOCS:
return name else:
for name in container.manifest_items_with_property('calibre:title-page'):
return name from calibre.ebooks.oeb.polish.toc import get_landmarks for landmark in get_landmarks(container):
if landmark['type'] == 'cover' and mm.get(landmark['dest'], '').lower() in OEB_DOCS:
return landmark['dest']

def fix_conversion_titlepage_links_in_nav(container):
from calibre.ebooks.oeb.polish.toc import find_existing_nav_toc cover_page_name = find_cover_page(container)
if not cover_page_name:
return nav_page_name = find_existing_nav_toc(container)
if not nav_page_name:
return for elem in container.parsed(nav_page_name).xpath('//*[@data-calibre-removed-titlepage]'):
elem.attrib.pop('data-calibre-removed-titlepage')
elem.set('href', container.name_to_href(cover_page_name, nav_page_name))
container.dirty(nav_page_name)

def find_cover_image_in_page(container, cover_page):
root = container.parsed(cover_page)
body = XPath('//h:body')(root)
if len(body) != 1:
return body = body[0]
images = []
for img in XPath('descendant::h:img[@src]|descendant::svg:svg/descendant::svg:image')(body):
href = img.get('src') or img.get(XLINK('href'))
if href:
name = container.href_to_name(href, base=cover_page)
images.append(name)
text = re.sub(r'\s+', '', xml2text(body))
if text or len(images) > 1:
# Document has more content than a single image return if images:
return images[0]

def clean_opf(container):
'Remove all references to covers from the OPF'
manifest_id_map = container.manifest_id_map for meta in container.opf_xpath('//opf:meta[@name="cover" and @content]'):
name = manifest_id_map.get(meta.get('content', None), None)
container.remove_from_xml(meta)
if name and name in container.name_path_map:
yield name gtm = container.guide_type_map for ref in container.opf_xpath('//opf:guide/opf:reference[@type]'):
typ = ref.get('type', '')
if typ.lower() in COVER_TYPES:
container.remove_from_xml(ref)
name = gtm.get(typ, None)
if name and name in container.name_path_map:
yield name ver = container.opf_version_parsed if ver.major > 2:
removed_names = container.apply_unique_properties(None, 'cover-image', 'calibre:title-page')[0]
for name in removed_names:
yield name container.dirty(container.opf_name)

def create_epub_cover(container, cover_path, existing_image, options=None):
from calibre.ebooks.conversion.config import load_defaults from calibre.ebooks.oeb.transforms.cover import CoverManager try:
ext = cover_path.rpartition('.')[-1].lower()
except Exception:
ext = 'jpeg'
cname, tname = 'cover.' + ext, 'titlepage.xhtml'
recommended_folders = get_recommended_folders(container, (cname, tname))

if existing_image:
raster_cover = existing_image manifest_id = {v:k for k, v in container.manifest_id_map.items()}[existing_image]
raster_cover_item = container.opf_xpath(f'//opf:manifest/*[@id="{manifest_id}"]')[0]
else:
folder = recommended_folders[cname]
if folder:
cname = folder + '/' + cname raster_cover_item = container.generate_item(cname, id_prefix='cover')
raster_cover = container.href_to_name(raster_cover_item.get('href'), container.opf_name)

with container.open(raster_cover, 'wb') as dest:
if callable(cover_path):
cover_path('write_image', dest)
else:
with open(cover_path, 'rb') as src:
shutil.copyfileobj(src, dest)
if options is None:
opts = load_defaults('epub_output')
keep_aspect = opts.get('preserve_cover_aspect_ratio', False)
no_svg = opts.get('no_svg_cover', False)
else:
keep_aspect = options.get('keep_aspect', False)
no_svg = options.get('no_svg', False)
if no_svg:
style = 'style="height: 100%%"'
templ = CoverManager.NONSVG_TEMPLATE.replace('__style__', style)
has_svg = False elif callable(cover_path):
templ = (options or {}).get('template', CoverManager.SVG_TEMPLATE)
has_svg = 'xlink:href' in templ else:
width, height = 600, 800 has_svg = True try:
if existing_image:
width, height = identify(container.raw_data(existing_image, decode=False))[1:]
else:
with open(cover_path, 'rb') as csrc:
width, height = identify(csrc)[1:]
except Exception:
container.log.exception('Failed to get width and height of cover')
ar = 'xMidYMid meet' if keep_aspect else 'none'
templ = CoverManager.SVG_TEMPLATE.replace('__ar__', ar)
templ = templ.replace('__viewbox__', f'0 0 {width} {height}')
templ = templ.replace('__width__', str(width))
templ = templ.replace('__height__', str(height))
folder = recommended_folders[tname]
if folder:
tname = folder + '/' + tname titlepage_item = container.generate_item(tname, id_prefix='titlepage')
titlepage = container.href_to_name(titlepage_item.get('href'), container.opf_name)
raw = templ % container.name_to_href(raster_cover, titlepage)
with container.open(titlepage, 'wb') as f:
if not isinstance(raw, bytes):
raw = raw.encode('utf-8')
f.write(raw)

# We have to make sure the raster cover item has id="cover" for the moron
# that wrote the Nook firmware if raster_cover_item.get('id') != 'cover':
from calibre.ebooks.oeb.base import uuid_id newid = uuid_id()
for item in container.opf_xpath('//*[@id="cover"]'):
item.set('id', newid)
for item in container.opf_xpath('//*[@idref="cover"]'):
item.set('idref', newid)
raster_cover_item.set('id', 'cover')

spine = container.opf_xpath('//opf:spine')[0]
ref = spine.makeelement(OPF('itemref'), idref=titlepage_item.get('id'))
container.insert_into_xml(spine, ref, index=0)
ver = container.opf_version_parsed if ver.major < 3:
guide = container.opf_get_or_create('guide')
container.insert_into_xml(guide, guide.makeelement(
OPF('reference'), type='cover', title=_('Cover'), href=container.name_to_href(titlepage, base=container.opf_name)))
metadata = container.opf_get_or_create('metadata')
meta = metadata.makeelement(OPF('meta'), name='cover')
meta.set('content', raster_cover_item.get('id'))
container.insert_into_xml(metadata, meta)
else:
container.apply_unique_properties(raster_cover, 'cover-image')
container.apply_unique_properties(titlepage, 'calibre:title-page')
if has_svg:
container.add_properties(titlepage, 'svg')

return raster_cover, titlepage def remove_cover_image_in_page(container, page, cover_images):
for img in container.parsed(page).xpath('//*[local-name()="img" and @src]'):
href = img.get('src')
name = container.href_to_name(href, page)
if name in cover_images:
img.getparent().remove(img)
break def has_epub_cover(container):
if find_cover_image(container):
return True if find_cover_page(container):
return True spine_items = tuple(container.spine_items)
if spine_items:
candidate = container.abspath_to_name(spine_items[0])
if find_cover_image_in_page(container, candidate) is not None:
return True return False def set_epub_cover(container, cover_path, report, options=None, image_callback=None):
existing_image = options is not None and options.get('existing_image', False)
if existing_image:
existing_image = cover_path cover_image = find_cover_image(container)
cover_page = find_cover_page(container)
wrapped_image = extra_cover_page = None updated = False log = container.log possible_removals = set(clean_opf(container))
possible_removals
# TODO: Handle possible_removals and also iterate over links in the removed
# pages and handle possibly removing stylesheets referred to by them.

image_callback_called = False spine_items = tuple(container.spine_items)
if cover_page is None and spine_items:
# Check if the first item in the spine is a simple cover wrapper candidate = container.abspath_to_name(spine_items[0])
if find_cover_image_in_page(container, candidate) is not None:
cover_page = candidate if cover_page is not None:
log('Found existing cover page')
wrapped_image = find_cover_image_in_page(container, cover_page)

if len(spine_items) > 1:
# Look for an extra cover page c = container.abspath_to_name(spine_items[1])
if c != cover_page:
candidate = find_cover_image_in_page(container, c)
if candidate and candidate in {wrapped_image, cover_image}:
log('Found an extra cover page that is a simple wrapper, removing it')
# This page has only a single image and that image is the
# cover image, remove it.
container.remove_item(c)
extra_cover_page = c spine_items = spine_items[:1] + spine_items[2:]
elif candidate is None:
# Remove the cover image if it is the first image in this
# page remove_cover_image_in_page(container, c, {wrapped_image, cover_image})

if wrapped_image is not None:
# The cover page is a simple wrapper around a single cover image,
# we can remove it safely.
log(f'Existing cover page {cover_page} is a simple wrapper, removing it')
container.remove_item(cover_page)
if wrapped_image != existing_image:
if image_callback is not None and not image_callback_called:
image_callback(cover_image, wrapped_image)
image_callback_called = True container.remove_item(wrapped_image)
updated = True if image_callback is not None and not image_callback_called:
image_callback_called = True image_callback(cover_image, wrapped_image)
if cover_image and cover_image != wrapped_image:
# Remove the old cover image if cover_image != existing_image:
container.remove_item(cover_image)

# Insert the new cover raster_cover, titlepage = create_epub_cover(container, cover_path, existing_image, options=options)

report(_('Cover updated') if updated else _('Cover inserted'))

# Replace links to the old cover image/cover page link_sub = {s:d for s, d in { cover_page:titlepage, wrapped_image:raster_cover, cover_image:raster_cover, extra_cover_page:titlepage}.items()
if s is not None and s != d}
if link_sub:
replace_links(container, link_sub, frag_map=lambda x, y:None)
return raster_cover, titlepage

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 시작 »
- 모듈 코드 »
- calibre.ebooks.oeb.polish.css

calibre.ebooks.oeb.polish.css의 소스 코드

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'

import re from collections import defaultdict from functools import lru_cache, partial from operator import itemgetter from css_parser.css import CSSRule, CSSStyleDeclaration from css_selectors import Select, SelectorError, SelectorSyntaxError, parse from calibre import force_unicode from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES, XHTML, css_text from calibre.ebooks.oeb.normalize_css import normalize_filter_css, normalizers from calibre.ebooks.oeb.polish.pretty import pretty_script_or_style, pretty_xml_tree, serialize from calibre.utils.icu import lower as icu_lower from calibre.utils.icu import numeric_sort_key from calibre.utils.localization import ngettext class SelectorStatus:
any_used: bool = False any_unused: bool = False def mark_used_selectors(rules, log, select):
ans = SelectorStatus()
for rule in rules:
for selector in rule.selectorList:
if getattr(selector, 'calibre_used', False):
ans.any_used = True continue try:
if select.has_matches(selector.selectorText):
selector.calibre_used = True ans.any_used = True else:
ans.any_unused = True selector.calibre_used = False except SelectorError:
# Cannot parse/execute this selector, be safe and assume it
# matches something selector.calibre_used = True ans.any_used = True return ans def get_imported_sheets(name, container, sheets, recursion_level=10, sheet=None):
ans = set()
sheet = sheet or sheets[name]
for rule in sheet.cssRules.rulesOfType(CSSRule.IMPORT_RULE):
if rule.href:
iname = container.href_to_name(rule.href, name)
if iname in sheets:
ans.add(iname)
if recursion_level > 0:
for imported_sheet in tuple(ans):
ans |= get_imported_sheets(imported_sheet, container, sheets, recursion_level=recursion_level-1)
ans.discard(name)
return ans def merge_declarations(first, second):
for prop in second.getProperties():
first.setProperty(prop)

def merge_identical_selectors(sheet):
' Merge rules that have identical selectors '
selector_map = defaultdict(list)
for rule in sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE):
selector_map[rule.selectorText].append(rule)
remove = []
for rule_group in selector_map.values():
if len(rule_group) > 1:
for i in range(1, len(rule_group)):
merge_declarations(rule_group[0].style, rule_group[i].style)
remove.append(rule_group[i])
for rule in remove:
sheet.cssRules.remove(rule)
return len(remove)

def merge_identical_properties(sheet):
' Merge rules having identical properties '
properties_map = defaultdict(list)

def declaration_key(declaration):
return tuple(sorted(
((prop.name, prop.propertyValue.value) for prop in declaration.getProperties()), key=itemgetter(0)
))

for idx, rule in enumerate(sheet.cssRules):
if rule.type == CSSRule.STYLE_RULE:
properties_map[declaration_key(rule.style)].append((idx, rule))

removals = []
num_merged = 0 for rule_group in properties_map.values():
if len(rule_group) < 2:
continue num_merged += len(rule_group)
selectors = rule_group[0][1].selectorList seen = {s.selectorText for s in selectors}
rules = iter(rule_group)
next(rules)
for idx, rule in rules:
removals.append(idx)
for s in rule.selectorList:
q = s.selectorText if q not in seen:
seen.add(q)
selectors.append(s)
for idx in sorted(removals, reverse=True):
sheet.cssRules.pop(idx)
return num_merged def remove_unused_selectors_and_rules(rules_container, rules, removal_stats):
ans = SelectorStatus()
for r in rules:
removals = []
for i, sel in enumerate(r.selectorList):
if getattr(sel, 'calibre_used', True):
ans.any_used = True else:
removals.append(i)
if removals:
ans.any_unused = True if len(removals) == len(r.selectorList):
rules_container.remove(r)
removal_stats['rules'] += 1 else:
removal_stats['selectors'] += len(removals)
for i in reversed(removals):
del r.selectorList[i]
return ans

[문서]
def remove_unused_css( container, report=None, remove_unused_classes=False, merge_rules=False, merge_rules_with_identical_properties=False, remove_unreferenced_sheets=False,
):
'''
Remove all unused CSS rules from the book. An unused CSS rule is one that does not match any actual content.

:param report: An optional callable that takes a single argument. It is called with information about the operations being performed.
:param remove_unused_classes: If True, class attributes in the HTML that do not match any CSS rules are also removed.
:param merge_rules: If True, rules with identical selectors are merged.
:param merge_rules_with_identical_properties: If True, rules with identical properties are merged.
:param remove_unreferenced_sheets: If True, stylesheets that are not referenced by any content are removed
'''
report = report or (lambda x:x)

def safe_parse(name):
try:
return container.parsed(name)
except TypeError:
pass sheets = {name:safe_parse(name) for name, mt in container.mime_map.items() if mt in OEB_STYLES}
sheets = {k:v for k, v in sheets.items() if v is not None}
num_merged = num_rules_merged = 0 if merge_rules:
for name, sheet in sheets.items():
num = merge_identical_selectors(sheet)
if num:
container.dirty(name)
num_merged += num if merge_rules_with_identical_properties:
for name, sheet in sheets.items():
num = merge_identical_properties(sheet)
if num:
container.dirty(name)
num_rules_merged += num import_map = {name:get_imported_sheets(name, container, sheets) for name in sheets}
unreferenced_sheets = set(sheets)
if remove_unused_classes:
class_map = {name:{icu_lower(x) for x in classes_in_rule_list(sheet.cssRules)} for name, sheet in sheets.items()}
style_rules = {name:tuple(sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE)) for name, sheet in sheets.items()}

removal_stats = {'rules': 0, 'selectors': 0}
num_of_removed_classes = 0 for name, mt in container.mime_map.items():
if mt not in OEB_DOCS:
continue root = container.parsed(name)
select = Select(root, ignore_inappropriate_pseudo_classes=True)
used_classes = set()
for style in root.xpath('//*[local-name()="style"]'):
if style.get('type', 'text/css') == 'text/css' and style.text:
sheet = container.parse_css(style.text)
if merge_rules:
num = merge_identical_selectors(sheet)
if num:
num_merged += num container.dirty(name)
if merge_rules_with_identical_properties:
num = merge_identical_properties(sheet)
if num:
num_rules_merged += num container.dirty(name)
if remove_unused_classes:
used_classes |= {icu_lower(x) for x in classes_in_rule_list(sheet.cssRules)}
imports = get_imported_sheets(name, container, sheets, sheet=sheet)
for imported_sheet in imports:
unreferenced_sheets.discard(imported_sheet)
mark_used_selectors(style_rules[imported_sheet], container.log, select)
if remove_unused_classes:
used_classes |= class_map[imported_sheet]
rules = tuple(sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE))
if mark_used_selectors(rules, container.log, select).any_unused:
remove_unused_selectors_and_rules(sheet.cssRules, rules, removal_stats)
style.text = force_unicode(sheet.cssText, 'utf-8')
pretty_script_or_style(container, style)
container.dirty(name)

for link in root.xpath('//*[local-name()="link" and @href]'):
sname = container.href_to_name(link.get('href'), name)
if sname not in sheets:
continue mark_used_selectors(style_rules[sname], container.log, select)
if remove_unused_classes:
used_classes |= class_map[sname]
unreferenced_sheets.discard(sname)

for iname in import_map[sname]:
unreferenced_sheets.discard(iname)
mark_used_selectors(style_rules[iname], container.log, select)
if remove_unused_classes:
used_classes |= class_map[iname]

if remove_unused_classes:
for elem in root.xpath('//*[@class]'):
original_classes, classes = elem.get('class', '').split(), []
for x in original_classes:
if icu_lower(x) in used_classes:
classes.append(x)
if len(classes) != len(original_classes):
if classes:
elem.set('class', ' '.join(classes))
else:
del elem.attrib['class']
num_of_removed_classes += len(original_classes) - len(classes)
container.dirty(name)

for name, sheet in sheets.items():
if name in unreferenced_sheets:
continue q = remove_unused_selectors_and_rules(sheet.cssRules, style_rules[name], removal_stats)
if q.any_unused:
container.dirty(name)
num_sheets_removed = 0 if remove_unreferenced_sheets and unreferenced_sheets:
num_sheets_removed += len(unreferenced_sheets)
for uname in unreferenced_sheets:
container.remove_item(uname)

num_changes = num_merged + num_of_removed_classes + num_rules_merged + removal_stats['rules'] + removal_stats['selectors'] + num_sheets_removed if num_changes > 0:
if removal_stats['rules']:
report(ngettext('Removed one unused CSS style rule', 'Removed {} unused CSS style rules', removal_stats['rules']).format(removal_stats['rules']))
if removal_stats['selectors']:
report(ngettext('Removed one unused CSS selector', 'Removed {} unused CSS selectors', removal_stats['selectors']).format(removal_stats['selectors']))
if num_of_removed_classes > 0:
report(ngettext('Removed one unused class from the HTML', 'Removed {} unused classes from the HTML', num_of_removed_classes).format(num_of_removed_classes))
if num_merged > 0:
report(ngettext('Merged one CSS style rule with identical selectors', 'Merged {} CSS style rules with identical selectors', num_merged).format(num_merged))
if num_rules_merged > 0:
report(ngettext('Merged one CSS style rule with identical properties', 'Merged {} CSS style rules with identical properties', num_rules_merged).format(num_rules_merged))
if num_sheets_removed:
report(ngettext('Removed one unreferenced stylesheet', 'Removed {} unreferenced stylesheets', num_sheets_removed).format(num_sheets_removed))
if not removal_stats['rules']:
report(_('No unused CSS style rules found'))
if not removal_stats['selectors']:
report(_('No unused CSS selectors found'))
if remove_unused_classes and num_of_removed_classes == 0:
report(_('No unused class attributes found'))
if merge_rules and num_merged == 0:
report(_('No style rules that could be merged found'))
if remove_unreferenced_sheets and num_sheets_removed == 0:
report(_('No unused stylesheets found'))
return num_changes > 0def filter_declaration(style, properties=()):
changed = False for prop in properties:
if style.removeProperty(prop) != '':
changed = True all_props = set(style.keys())
for prop in style.getProperties():
n = normalizers.get(prop.name, None)
if n is not None:
normalized = n(prop.name, prop.propertyValue)
removed = properties.intersection(set(normalized))
if removed:
changed = True style.removeProperty(prop.name)
for prop in set(normalized) - removed - all_props:
style.setProperty(prop, normalized[prop])
return changed def filter_sheet(sheet, properties=()):
from css_parser.css import CSSRule changed = False remove = []
for rule in sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE):
if filter_declaration(rule.style, properties):
changed = True if rule.style.length == 0:
remove.append(rule)
for rule in remove:
sheet.cssRules.remove(rule)
return changed def transform_inline_styles(container, name, transform_sheet, transform_style):
root = container.parsed(name)
changed = False for style in root.xpath('//*[local-name()="style"]'):
if style.text and (style.get('type') or 'text/css').lower() == 'text/css':
sheet = container.parse_css(style.text)
if transform_sheet(sheet):
changed = True style.text = force_unicode(sheet.cssText, 'utf-8')
pretty_script_or_style(container, style)
for elem in root.xpath('//*[@style]'):
text = elem.get('style', None)
if text:
style = container.parse_css(text, is_declaration=True)
if transform_style(style):
changed = True if style.length == 0:
del elem.attrib['style']
else:
elem.set('style', force_unicode(style.getCssText(separator=' '), 'utf-8'))
return changed def transform_css(container, transform_sheet=None, transform_style=None, names=()):
if not names:
types = OEB_STYLES | OEB_DOCS names = []
for name, mt in container.mime_map.items():
if mt in types:
names.append(name)

doc_changed = False for name in names:
mt = container.mime_map[name]
if mt in OEB_STYLES:
sheet = container.parsed(name)
if transform_sheet(sheet):
container.dirty(name)
doc_changed = True elif mt in OEB_DOCS:
if transform_inline_styles(container, name, transform_sheet, transform_style):
container.dirty(name)
doc_changed = True return doc_changed

[문서]
def filter_css(container, properties, names=()):
'''
Remove the specified CSS properties from all CSS rules in the book.

:param properties: Set of properties to remove. For example: :code:`{'font-family', 'color'}`.
:param names: The files from which to remove the properties. Defaults to all HTML and CSS files in the book.
'''
properties = normalize_filter_css(properties)
return transform_css(container, transform_sheet=partial(filter_sheet, properties=properties), transform_style=partial(filter_declaration, properties=properties), names=names)

def _classes_in_selector(selector, classes):
for attr in ('selector', 'subselector', 'parsed_tree'):
s = getattr(selector, attr, None)
if s is not None:
_classes_in_selector(s, classes)
cn = getattr(selector, 'class_name', None)
if cn is not None:
classes.add(cn)

@lru_cache(maxsize=4096)
def classes_in_selector(text):
classes = set()
try:
for selector in parse(text):
_classes_in_selector(selector, classes)
except SelectorSyntaxError:
pass return classes def classes_in_rule_list(css_rules):
classes = set()
for rule in css_rules:
if rule.type == rule.STYLE_RULE:
classes |= classes_in_selector(rule.selectorText)
elif hasattr(rule, 'cssRules'):
classes |= classes_in_rule_list(rule.cssRules)
return classes def iter_declarations(sheet_or_rule):
if hasattr(sheet_or_rule, 'cssRules'):
for rule in sheet_or_rule.cssRules:
yield from iter_declarations(rule)
elif hasattr(sheet_or_rule, 'style'):
yield sheet_or_rule.style elif isinstance(sheet_or_rule, CSSStyleDeclaration):
yield sheet_or_rule def remove_property_value(prop, predicate):
''' Remove the Values that match the predicate from this property. If all values of the property would be removed, the property is removed from its parent instead. Note that this means the property must have a parent (a
CSSStyleDeclaration). '''
removed_vals = list(filter(predicate, prop.propertyValue))
if len(removed_vals) == len(prop.propertyValue):
prop.parent.removeProperty(prop.name)
else:
x = css_text(prop.propertyValue)
for v in removed_vals:
x = x.replace(css_text(v), '').strip()
prop.propertyValue.cssText = x return bool(removed_vals)

RULE_PRIORITIES = {t:i for i, t in enumerate((CSSRule.COMMENT, CSSRule.CHARSET_RULE, CSSRule.IMPORT_RULE, CSSRule.NAMESPACE_RULE))}

def sort_sheet(container, sheet_or_text):
''' Sort the rules in a stylesheet. Note that in the general case this can change the effective styles, but for most common sheets, it should be safe.
'''
sheet = container.parse_css(sheet_or_text) if isinstance(sheet_or_text, str) else sheet_or_text def text_sort_key(x):
return numeric_sort_key(str(x or ''))

def selector_sort_key(x):
return (x.specificity, text_sort_key(x.selectorText))

def rule_sort_key(rule):
primary = RULE_PRIORITIES.get(rule.type, len(RULE_PRIORITIES))
secondary = text_sort_key(getattr(rule, 'atkeyword', '') or '')
tertiary = None if rule.type == CSSRule.STYLE_RULE:
primary += 1 selectors = sorted(rule.selectorList, key=selector_sort_key)
tertiary = selector_sort_key(selectors[0])
rule.selectorText = ', '.join(s.selectorText for s in selectors)
elif rule.type == CSSRule.FONT_FACE_RULE:
try:
tertiary = text_sort_key(rule.style.getPropertyValue('font-family'))
except Exception:
pass return primary, secondary, tertiary sheet.cssRules.sort(key=rule_sort_key)
return sheet def add_stylesheet_links(container, name, text):
root = container.parse_xhtml(text, name)
head = root.xpath('//*[local-name() = "head"]')
if not head:
return head = head[0]
sheets = tuple(container.manifest_items_of_type(lambda mt: mt in OEB_STYLES))
if not sheets:
return for sname in sheets:
link = head.makeelement(XHTML('link'), type='text/css', rel='stylesheet', href=container.name_to_href(sname, name))
head.append(link)
pretty_xml_tree(head)
return serialize(root, 'text/html')

def rename_class_in_rule_list(css_rules, old_name, new_name):
# this regex will not match class names inside attribute value selectors
# and it will match id selectors that contain .old_name but its the best
# that can be done without implementing a full parser for CSS selectors pat = re.compile(rf'(?<=\.){re.escape(old_name)}(?:\W|$)')

def repl(m):
return m.group().replace(old_name, new_name)

changed = False for rule in css_rules:
if rule.type == rule.STYLE_RULE:
old = rule.selectorText q = pat.sub(repl, old)
if q != old:
changed = True rule.selectorText = q elif hasattr(rule, 'cssRules'):
if rename_class_in_rule_list(rule.cssRules, old_name, new_name):
changed = True return changed def rename_class_in_doc(container, root, old_name, new_name):
changed = False pat = re.compile(rf'(?:^|\W){re.escape(old_name)}(?:\W|$)')

def repl(m):
return m.group().replace(old_name, new_name)

for elem in root.xpath('//*[@class]'):
old = elem.get('class')
if old:
new = pat.sub(repl, old)
if new != old:
changed = True elem.set('class', new)
for style in root.xpath('//*[local-name()="style"]'):
if style.get('type', 'text/css') == 'text/css' and style.text:
sheet = container.parse_css(style.text)
if rename_class_in_rule_list(sheet.cssRules, old_name, new_name):
changed = True style.text = force_unicode(sheet.cssText, 'utf-8')
return changed def rename_class(container, old_name, new_name):
changed = False if not old_name or old_name == new_name:
return changed for sheet_name in container.manifest_items_of_type(lambda mt: mt in OEB_STYLES):
sheet = container.parsed(sheet_name)
if rename_class_in_rule_list(sheet.cssRules, old_name, new_name):
container.dirty(sheet_name)
changed = True for doc_name in container.manifest_items_of_type(lambda mt: mt in OEB_DOCS):
doc = container.parsed(doc_name)
if rename_class_in_doc(container, doc, old_name, new_name):
container.dirty(doc_name)
changed = True return changed

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 시작 »
- 모듈 코드 »
- calibre.ebooks.oeb.polish.fonts

calibre.ebooks.oeb.polish.fonts의 소스 코드

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'

from tinycss.fonts3 import parse_font, parse_font_family, serialize_font, serialize_font_family from calibre.ebooks.oeb.base import css_text from calibre.ebooks.oeb.normalize_css import normalize_font from calibre.ebooks.oeb.polish.container import OEB_DOCS, OEB_STYLES def unquote(x):
if x and len(x) > 1 and x[0] == x[-1] and x[0] in ('"', "'"):
x = x[1:-1]
return x def font_family_data_from_declaration(style, families):
font_families = []
f = style.getProperty('font')
if f is not None:
f = normalize_font(f.propertyValue, font_family_as_list=True).get('font-family', None)
if f is not None:
font_families = [unquote(x) for x in f]
f = style.getProperty('font-family')
if f is not None:
font_families = parse_font_family(css_text(f.propertyValue))

for f in font_families:
families[f] = families.get(f, False)

def font_family_data_from_sheet(sheet, families):
for rule in sheet.cssRules:
if rule.type == rule.STYLE_RULE:
font_family_data_from_declaration(rule.style, families)
elif rule.type == rule.FONT_FACE_RULE:
ff = rule.style.getProperty('font-family')
if ff is not None:
for f in parse_font_family(css_text(ff.propertyValue)):
families[f] = True def font_family_data(container):
families = {}
for name, mt in container.mime_map.items():
if mt in OEB_STYLES:
sheet = container.parsed(name)
font_family_data_from_sheet(sheet, families)
elif mt in OEB_DOCS:
root = container.parsed(name)
for style in root.xpath('//*[local-name() = "style"]'):
if style.text and style.get('type', 'text/css').lower() == 'text/css':
sheet = container.parse_css(style.text)
font_family_data_from_sheet(sheet, families)
for style in root.xpath('//*/@style'):
if style:
style = container.parse_css(style, is_declaration=True)
font_family_data_from_declaration(style, families)
return families def change_font_in_declaration(style, old_name, new_name=None):
changed = False ff = style.getProperty('font-family')
if ff is not None:
fams = parse_font_family(css_text(ff.propertyValue))
nfams = list(filter(None, [new_name if x == old_name else x for x in fams]))
if fams != nfams:
if nfams:
ff.propertyValue.cssText = serialize_font_family(nfams)
else:
style.removeProperty(ff.name)
changed = True ff = style.getProperty('font')
if ff is not None:
props = parse_font(css_text(ff.propertyValue))
fams = props.get('font-family') or []
nfams = list(filter(None, [new_name if x == old_name else x for x in fams]))
if fams != nfams:
props['font-family'] = nfams if nfams:
ff.propertyValue.cssText = serialize_font(props)
else:
style.removeProperty(ff.name)
changed = True return changed def remove_embedded_font(container, sheet, rule, sheet_name):
src = getattr(rule.style.getProperty('src'), 'value', None)
if src is not None:
if src.startswith('url('):
src = src[4:-1]
sheet.cssRules.remove(rule)
if src:
src = unquote(src)
name = container.href_to_name(src, sheet_name)
if container.has_name(name):
container.remove_item(name)

def change_font_in_sheet(container, sheet, old_name, new_name, sheet_name):
changed = False removals = []
for rule in sheet.cssRules:
if rule.type == rule.STYLE_RULE:
changed |= change_font_in_declaration(rule.style, old_name, new_name)
elif rule.type == rule.FONT_FACE_RULE:
ff = rule.style.getProperty('font-family')
if ff is not None:
families = set(parse_font_family(css_text(ff.propertyValue)))
if old_name in families:
changed = True removals.append(rule)
for rule in reversed(removals):
remove_embedded_font(container, sheet, rule, sheet_name)
return changed

[문서]
def change_font(container, old_name, new_name=None):
'''
Change a font family from old_name to new_name. Changes all occurrences of the font family in stylesheets, style tags and style attributes.
If the old_name refers to an embedded font, it is removed. You can set new_name to None to remove the font family instead of changing it.
'''
changed = False for name, mt in tuple(container.mime_map.items()):
if mt in OEB_STYLES:
sheet = container.parsed(name)
if change_font_in_sheet(container, sheet, old_name, new_name, name):
container.dirty(name)
changed = True elif mt in OEB_DOCS:
root = container.parsed(name)
for style in root.xpath('//*[local-name() = "style"]'):
if style.text and style.get('type', 'text/css').lower() == 'text/css':
sheet = container.parse_css(style.text)
if change_font_in_sheet(container, sheet, old_name, new_name, name):
container.dirty(name)
changed = True for elem in root.xpath('//*[@style]'):
style = elem.get('style', '')
if style:
style = container.parse_css(style, is_declaration=True)
if change_font_in_declaration(style, old_name, new_name):
style = css_text(style).strip().rstrip(';').strip()
if style:
elem.set('style', style)
else:
del elem.attrib['style']
container.dirty(name)
changed = True return changed

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 시작 »
- 모듈 코드 »
- calibre.ebooks.oeb.polish.jacket

calibre.ebooks.oeb.polish.jacket의 소스 코드

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

from calibre.customize.ui import output_profiles from calibre.ebooks.conversion.config import load_defaults from calibre.ebooks.oeb.base import OPF, XPath from calibre.ebooks.oeb.polish.cover import find_cover_page from calibre.ebooks.oeb.transforms.jacket import referenced_images from calibre.ebooks.oeb.transforms.jacket import render_jacket as render def render_jacket(container, jacket):
mi = container.mi ps = load_defaults('page_setup')
op = ps.get('output_profile', 'default')
opmap = {x.short_name:x for x in output_profiles()}
output_profile = opmap.get(op, opmap['default'])
root = render(mi, output_profile)
for img, path in referenced_images(root):
container.log(f'Embedding referenced image: {path} into jacket')
ext = path.rpartition('.')[-1]
jacket_item = container.generate_item('jacket_image.'+ext, id_prefix='jacket_img')
name = container.href_to_name(jacket_item.get('href'), container.opf_name)
with open(path, 'rb') as f:
container.parsed_cache[name] = f.read()
container.commit_item(name)
href = container.name_to_href(name, jacket)
img.set('src', href)
return root def is_legacy_jacket(root):
return len(root.xpath(
'//*[starts-with(@class,"calibrerescale") and (local-name()="h1" or local-name()="h2")]')) > 0 def is_current_jacket(root):
return len(XPath(
'//h:meta[@name="calibre-content" and @content="jacket"]')(root)) > 0 def find_existing_jacket(container):
for item in container.spine_items:
name = container.abspath_to_name(item)
if container.book_type == 'azw3':
root = container.parsed(name)
if is_current_jacket(root):
return name elif name.rpartition('/')[-1].startswith('jacket') and name.endswith('.xhtml'):
root = container.parsed(name)
if is_current_jacket(root) or is_legacy_jacket(root):
return name def replace_jacket(container, name):
root = render_jacket(container, name)
container.parsed_cache[name] = root container.dirty(name)

[문서]
def remove_jacket(container):
' Remove an existing jacket, if any. Returns False if no existing jacket was found. '
name = find_existing_jacket(container)
if name is not None:
remove_jacket_images(container, name)
container.remove_item(name)
return True return False
def remove_jacket_images(container, name):
root = container.parsed_cache[name]
for img in root.xpath('//*[local-name() = "img" and @src]'):
iname = container.href_to_name(img.get('src'), name)
if container.has_name(iname):
container.remove_item(iname)

[문서]
def add_or_replace_jacket(container):
''' Either create a new jacket from the book's metadata or replace an existing jacket. Returns True if an existing jacket was replaced. '''
name = find_existing_jacket(container)
found = True if name is None:
jacket_item = container.generate_item('jacket.xhtml', id_prefix='jacket')
name = container.href_to_name(jacket_item.get('href'), container.opf_name)
found = False if found:
remove_jacket_images(container, name)

replace_jacket(container, name)
if not found:
# Insert new jacket into spine index = 0 sp = container.abspath_to_name(next(container.spine_items))
if sp == find_cover_page(container):
index = 1 itemref = container.opf.makeelement(OPF('itemref'), idref=jacket_item.get('id'))
container.insert_into_xml(container.opf_xpath('//opf:spine')[0], itemref, index=index)
return found

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 시작 »
- 모듈 코드 »
- calibre.ebooks.oeb.polish.pretty

calibre.ebooks.oeb.polish.pretty의 소스 코드

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'

import textwrap

# from lxml.etree import Element from calibre import force_unicode from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES, SVG, XHTML, XPNSMAP, barename, serialize from calibre.ebooks.oeb.polish.container import OPF_NAMESPACES from calibre.ebooks.oeb.polish.utils import guess_type from calibre.utils.icu import sort_key def isspace(x):
return not x.strip('\u0009\u000a\u000c\u000d\u0020')

def pretty_xml_tree(elem, level=0, indent=' '):
''' XML beautifier, assumes that elements that have children do not have textual content. Also assumes that there is no text immediately after closing tags. These are true for opf/ncx and container.xml files. If either of the assumptions are violated, there should be no data loss, but pretty printing won't produce optimal results.'''
if (not elem.text and len(elem) > 0) or (elem.text and isspace(elem.text)):
elem.text = '\n' + (indent * (level+1))
for i, child in enumerate(elem):
pretty_xml_tree(child, level=level+1, indent=indent)
if not child.tail or isspace(child.tail):
l = level + 1 if i == len(elem) - 1:
l -= 1 child.tail = '\n' + (indent * l)

def pretty_opf(root):
# Put all dc: tags first starting with title and author. Preserve order for
# the rest.
def dckey(x):
return {'title':0, 'creator':1}.get(barename(x.tag), 2)
for metadata in root.xpath('//opf:metadata', namespaces=OPF_NAMESPACES):
dc_tags = metadata.xpath('./*[namespace-uri()="{}"]'.format(OPF_NAMESPACES['dc']))
dc_tags.sort(key=dckey)
for x in reversed(dc_tags):
metadata.insert(0, x)

# Group items in the manifest spine_ids = root.xpath('//opf:spine/opf:itemref/@idref', namespaces=OPF_NAMESPACES)
spine_ids = {x:i for i, x in enumerate(spine_ids)}

def manifest_key(x):
mt = x.get('media-type', '')
href = x.get('href', '')
ext = href.rpartition('.')[-1].lower()
cat = 1000 if mt in OEB_DOCS:
cat = 0 elif mt == guess_type('a.ncx'):
cat = 1 elif mt in OEB_STYLES:
cat = 2 elif mt.startswith('image/'):
cat = 3 elif ext in {'otf', 'ttf', 'woff', 'woff2'}:
cat = 4 elif mt.startswith('audio/'):
cat = 5 elif mt.startswith('video/'):
cat = 6 if cat == 0:
i = spine_ids.get(x.get('id', None), 1000000000)
else:
i = sort_key(href)
return cat, i for manifest in root.xpath('//opf:manifest', namespaces=OPF_NAMESPACES):
try:
children = sorted(manifest, key=manifest_key)
except AttributeError:
continue # There are comments so don't sort since that would mess up the comments for x in reversed(children):
manifest.insert(0, x)

SVG_TAG = SVG('svg')
NON_NAMESPACED_BLOCK_TAGS = (
'address', 'article', 'aside', 'audio', 'blockquote', 'body', 'canvas', 'col', 'colgroup', 'dd',
'div', 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'li', 'svg',
'noscript', 'ol', 'output', 'p', 'pre', 'script', 'section', 'style', 'table', 'tbody', 'td',
'tfoot', 'th', 'thead', 'tr', 'ul', 'video', 'img'
)
BLOCK_TAGS = frozenset(map(XHTML, NON_NAMESPACED_BLOCK_TAGS))

def isblock(x):
if callable(x.tag) or not x.tag:
return True if x.tag in BLOCK_TAGS:
return True return False def has_only_blocks(x):
if hasattr(x.tag, 'split') and len(x) == 0:
# Tag with no children, return False if x.text and not isspace(x.text):
return False for child in x:
if not isblock(child) or (child.tail and not isspace(child.tail)):
return False return True def indent_for_tag(x):
prev = x.getprevious()
x = x.getparent().text if prev is None else prev.tail if not x:
return ''
s = x.rpartition('\n')[-1]
return s if isspace(s) else ''

def set_indent(elem, attr, indent):
x = getattr(elem, attr)
if not x:
x = indent else:
lines = x.splitlines()
if isspace(lines[-1]):
lines[-1] = indent else:
lines.append(indent)
x = '\n'.join(lines)
setattr(elem, attr, x)

def pretty_block(parent, level=1, indent=' '):
''' Surround block tags with blank lines and recurse into child block tags that contain only other block tags '''
if not parent.text or isspace(parent.text):
parent.text = ''
nn = '\n' if hasattr(parent.tag, 'strip') and barename(parent.tag) in {'tr', 'td', 'th'} else '\n\n'
parent.text = parent.text + nn + (indent * level)
for i, child in enumerate(parent):
if isblock(child) and has_only_blocks(child):
pretty_block(child, level=level+1, indent=indent)
elif child.tag == SVG_TAG:
pretty_xml_tree(child, level=level, indent=indent)
l = level if i == len(parent) - 1:
l -= 1 if not child.tail or isspace(child.tail):
child.tail = ''
child.tail = child.tail + nn + (indent * l)

def pretty_script_or_style(container, child):
if child.text:
indent = indent_for_tag(child)
if child.tag.endswith('style'):
child.text = force_unicode(pretty_css(container, '', child.text), 'utf-8')
child.text = textwrap.dedent(child.text)
child.text = '\n' + '\n'.join([(indent + x) if x else '' for x in child.text.splitlines()])
set_indent(child, 'text', indent)

def pretty_html_tree(container, root):
root.text = '\n\n'
for child in root:
child.tail = '\n\n'
if hasattr(child.tag, 'endswith') and child.tag.endswith('}head'):
pretty_xml_tree(child)
for body in root.findall('h:body', namespaces=XPNSMAP):
pretty_block(body)
# Special case the handling of a body that contains a single block tag
# with all content. In this case we prettify the containing block tag
# even if it has non block children.
if (len(body) == 1 and not callable(body[0].tag) and isblock(body[0]) and not has_only_blocks( body[0]) and barename(body[0].tag) not in (
'pre', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6') and len(body[0]) > 0):
pretty_block(body[0], level=2)

if container is not None:
# Handle <script> and <style> tags for child in root.xpath('//*[local-name()="script" or local-name()="style"]'):
pretty_script_or_style(container, child)

[문서]
def fix_html(container, raw):
' Fix any parsing errors in the HTML represented as a string in raw. Fixing is done using the HTML5 parsing algorithm. '
root = container.parse_xhtml(raw)
return serialize(root, 'text/html')

[문서]
def pretty_html(container, name, raw):
' Pretty print the HTML represented as a string in raw '
root = container.parse_xhtml(raw)
pretty_html_tree(container, root)
return serialize(root, 'text/html')

[문서]
def pretty_css(container, name, raw):
' Pretty print the CSS represented as a string in raw '
sheet = container.parse_css(raw)
return serialize(sheet, 'text/css')

[문서]
def pretty_xml(container, name, raw):
' Pretty print the XML represented as a string in raw. If ``name`` is the name of the OPF, extra OPF-specific prettying is performed. '
root = container.parse_xml(raw)
if name == container.opf_name:
pretty_opf(root)
pretty_xml_tree(root)
return serialize(root, 'text/xml')

[문서]
def fix_all_html(container):
' Fix any parsing errors in all HTML files in the container. Fixing is done using the HTML5 parsing algorithm. '
for name, mt in container.mime_map.items():
if mt in OEB_DOCS:
container.parsed(name)
container.dirty(name)

[문서]
def pretty_all(container):
' Pretty print all HTML/CSS/XML files in the container '
xml_types = {guess_type('a.ncx'), guess_type('a.xml'), guess_type('a.svg')}
for name, mt in container.mime_map.items():
prettied = False if mt in OEB_DOCS:
pretty_html_tree(container, container.parsed(name))
prettied = True elif mt in OEB_STYLES:
container.parsed(name)
prettied = True elif name == container.opf_name:
root = container.parsed(name)
pretty_opf(root)
pretty_xml_tree(root)
prettied = True elif mt in xml_types:
pretty_xml_tree(container.parsed(name))
prettied = True if prettied:
container.dirty(name)

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 시작 »
- 모듈 코드 »
- calibre.ebooks.oeb.polish.replace

calibre.ebooks.oeb.polish.replace의 소스 코드

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import codecs import os import posixpath import shutil from collections import Counter, defaultdict from functools import partial from urllib.parse import urlparse, urlunparse from calibre import sanitize_file_name from calibre.ebooks.chardet import strip_encoding_declarations from calibre.ebooks.oeb.base import css_text from calibre.ebooks.oeb.polish.css import iter_declarations, remove_property_value from calibre.ebooks.oeb.polish.utils import extract class LinkReplacer:

def __init__(self, base, container, link_map, frag_map):
self.base = base self.frag_map = frag_map self.link_map = link_map self.container = container self.replaced = False def __call__(self, url):
if url and url.startswith('#'):
repl = self.frag_map(self.base, url[1:])
if not repl or repl == url[1:]:
return url self.replaced = True return '#' + repl name = self.container.href_to_name(url, self.base)
if not name:
return url nname = self.link_map.get(name, None)
if not nname:
return url purl = urlparse(url)
href = self.container.name_to_href(nname, self.base)
if purl.fragment:
nfrag = self.frag_map(name, purl.fragment)
if nfrag:
href += f'#{nfrag}'
if href != url:
self.replaced = True return href class IdReplacer:

def __init__(self, base, container, id_map):
self.base, self.container, self.replaced = base, container, False self.id_map = id_map def __call__(self, url):
if url and url.startswith('#'):
repl = self.id_map.get(self.base, {}).get(url[1:])
if repl is None or repl == url[1:]:
return url self.replaced = True return '#' + repl name = self.container.href_to_name(url, self.base)
if not name:
return url id_map = self.id_map.get(name)
if id_map is None:
return url purl = urlparse(url)
nfrag = id_map.get(purl.fragment)
if nfrag is None:
return url purl = purl._replace(fragment=nfrag)
href = urlunparse(purl)
if href != url:
self.replaced = True return href class LinkRebaser:

def __init__(self, container, old_name, new_name):
self.old_name, self.new_name = old_name, new_name self.container = container self.replaced = False def __call__(self, url):
if url and url.startswith('#'):
return url purl = urlparse(url)
frag = purl.fragment name = self.container.href_to_name(url, self.old_name)
if not name:
return url if name == self.old_name:
name = self.new_name href = self.container.name_to_href(name, self.new_name)
if frag:
href += '#' + frag if href != url:
self.replaced = True return href

[문서]
def replace_links(container, link_map, frag_map=lambda name, frag:frag, replace_in_opf=False):
'''
Replace links to files in the container. Will iterate over all files in the container and change the specified links in them.

:param link_map: A mapping of old canonical name to new canonical name. For example: :code:`{'images/old.png': 'images/new.png'}`
:param frag_map: A callable that takes two arguments ``(name, anchor)`` and returns a new anchor. This is useful if you need to change the anchors in
HTML files. By default, it does nothing.
:param replace_in_opf: If False, links are not replaced in the OPF file.

'''
for name, media_type in container.mime_map.items():
if name == container.opf_name and not replace_in_opf:
continue repl = LinkReplacer(name, container, link_map, frag_map)
container.replace_links(name, repl)

def replace_ids(container, id_map):
'''
Replace all links in the container that pointed to the changed ids.

:param id_map: A mapping of {name:id_map} where each id_map is a mapping of {old_id:new_id}
:return: True iff at least one link was changed

'''
changed = False for name, media_type in container.mime_map.items():
repl = IdReplacer(name, container, id_map)
container.replace_links(name, repl)
if name == container.opf_name:
imap = id_map.get(name, {})
for item in container.opf_xpath('//*[@idref]'):
old_id = item.get('idref')
if old_id is not None:
new_id = imap.get(old_id)
if new_id is not None:
item.set('idref', new_id)
if repl.replaced:
changed = True return changed def smarten_punctuation(container, report):
from calibre.ebooks.conversion.preprocess import smarten_punctuation smartened = False for path in container.spine_items:
name = container.abspath_to_name(path)
changed = False with container.open(name, 'r+b') as f:
html = container.decode(f.read())
newhtml = smarten_punctuation(html, container.log)
if newhtml != html:
changed = True report(_('Smartened punctuation in: %s')%name)
newhtml = strip_encoding_declarations(newhtml)
f.seek(0)
f.truncate()
f.write(codecs.BOM_UTF8 + newhtml.encode('utf-8'))
if changed:
# Add an encoding declaration (it will be added automatically when
# serialized)
root = container.parsed(name)
for m in root.xpath('descendant::*[local-name()="meta" and @http-equiv]'):
m.getparent().remove(m)
container.dirty(name)
smartened = True if not smartened:
report(_('No punctuation that could be smartened found'))
return smartened

[문서]
def rename_files(container, file_map):
'''
Rename files in the container, automatically updating all links to them.

:param file_map: A mapping of old canonical name to new canonical name, for example: :code:`{'text/chapter1.html': 'chapter1.html'}`.
'''
overlap = set(file_map).intersection(set(file_map.values()))
if overlap:
raise ValueError('Circular rename detected. The files {} are both rename targets and destinations'.format(', '.join(overlap)))
for name, dest in file_map.items():
if container.exists(dest):
if name != dest and name.lower() == dest.lower():
# A case change on an OS with a case insensitive file-system.
continue raise ValueError(f'Cannot rename {name} to {dest} as {dest} already exists')
if len(tuple(file_map.values())) != len(set(file_map.values())):
raise ValueError('Cannot rename, the set of destination files contains duplicates')
link_map = {}
for current_name, new_name in file_map.items():
container.rename(current_name, new_name)
if new_name != container.opf_name: # OPF is handled by the container link_map[current_name] = new_name replace_links(container, link_map, replace_in_opf=True)

def replace_file(container, name, path, basename, force_mt=None):
dirname, base = name.rpartition('/')[0::2]
nname = sanitize_file_name(basename)
if dirname:
nname = dirname + '/' + nname with open(path, 'rb') as src:
if name != nname:
count = 0 b, e = nname.rpartition('.')[0::2]
while container.exists(nname):
count += 1 nname = b + f'_{count}.{e}'
rename_files(container, {name:nname})
mt = force_mt or container.guess_type(nname)
container.mime_map[nname] = mt for itemid, q in container.manifest_id_map.items():
if q == nname:
for item in container.opf_xpath(f'//opf:manifest/opf:item[@href and @id="{itemid}"]'):
item.set('media-type', mt)
container.dirty(container.opf_name)
with container.open(nname, 'wb') as dest:
shutil.copyfileobj(src, dest)

def mt_to_category(container, mt):
from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES from calibre.ebooks.oeb.polish.utils import OEB_FONTS, guess_type if mt in OEB_DOCS:
category = 'text'
elif mt in OEB_STYLES:
category = 'style'
elif mt in OEB_FONTS:
category = 'font'
elif mt == guess_type('a.opf'):
category = 'opf'
elif mt == guess_type('a.ncx'):
category = 'toc'
else:
category = mt.partition('/')[0]
return category

[문서]
def get_recommended_folders(container, names):
''' Return the folders that are recommended for the given filenames. The recommendation is based on where the majority of files of the same type are located in the container. If no files of a particular type are present, the recommended folder is assumed to be the folder containing the OPF file. '''
from calibre.ebooks.oeb.polish.utils import guess_type counts = defaultdict(Counter)
for name, mt in container.mime_map.items():
folder = name.rpartition('/')[0] if '/' in name else ''
counts[mt_to_category(container, mt)][folder] += 1 try:
opf_folder = counts['opf'].most_common(1)[0][0]
except KeyError:
opf_folder = ''

recommendations = {category:counter.most_common(1)[0][0] for category, counter in counts.items()}
return {n:recommendations.get(mt_to_category(container, guess_type(os.path.basename(n))), opf_folder) for n in names}

def normalize_case(container, val):

def safe_listdir(x):
try:
return os.listdir(x)
except OSError:
return ()

parts = val.split('/')
ans = []
for i in range(len(parts)):
q = '/'.join(parts[:i+1])
x = container.name_to_abspath(q)
xl = parts[i].lower()
candidates = [c for c in safe_listdir(os.path.dirname(x)) if c != parts[i] and c.lower() == xl]
ans.append(candidates[0] if candidates else parts[i])
return '/'.join(ans)

def rationalize_folders(container, folder_type_map):
all_names = set(container.mime_map)
new_names = set()
name_map = {}
for key in tuple(folder_type_map):
val = folder_type_map[key]
folder_type_map[key] = normalize_case(container, val)
for name in all_names:
if name.startswith('META-INF/'):
continue category = mt_to_category(container, container.mime_map[name])
folder = folder_type_map.get(category, None)
if folder is not None:
bn = posixpath.basename(name)
new_name = posixpath.join(folder, bn)
if new_name != name:
c = 0 while new_name in all_names or new_name in new_names:
c += 1 n, ext = bn.rpartition('.')[0::2]
new_name = posixpath.join(folder, f'{n}_{c}.{ext}')
name_map[name] = new_name new_names.add(new_name)
return name_map def remove_links_in_sheet(href_to_name, sheet, predicate):
import_rules_to_remove = []
changed = False for i, r in enumerate(sheet):
if r.type == r.IMPORT_RULE:
name = href_to_name(r.href)
if predicate(name, r.href, None):
import_rules_to_remove.append(i)
for i in sorted(import_rules_to_remove, reverse=True):
sheet.deleteRule(i)
changed = True for dec in iter_declarations(sheet):
changed = remove_links_in_declaration(href_to_name, dec, predicate) or changed return changed def remove_links_in_declaration(href_to_name, style, predicate):
def check_pval(v):
if v.type == v.URI:
name = href_to_name(v.uri)
return predicate(name, v.uri, None)
return False changed = False for p in tuple(style.getProperties(all=True)):
changed = remove_property_value(p, check_pval) or changed return changed def remove_links_to(container, predicate):
''' predicate must be a function that takes the arguments (name, href, fragment=None) and returns True iff the link should be removed '''
from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES, XHTML, XPath, iterlinks stylepath = XPath('//h:style')
styleattrpath = XPath('//*[@style]')
changed = set()
for name, mt in container.mime_map.items():
removed = False if mt in OEB_DOCS:
root = container.parsed(name)
for el, attr, href, pos in iterlinks(root, find_links_in_css=False):
hname = container.href_to_name(href, name)
frag = href.partition('#')[-1]
if predicate(hname, href, frag):
if attr is None:
el.text = None elif el.tag == XHTML('link') or el.tag == XHTML('img'):
extract(el)
else:
del el.attrib[attr]
removed = True for tag in stylepath(root):
if tag.text and (tag.get('type') or 'text/css').lower() == 'text/css':
sheet = container.parse_css(tag.text)
if remove_links_in_sheet(partial(container.href_to_name, base=name), sheet, predicate):
tag.text = css_text(sheet)
removed = True for tag in styleattrpath(root):
style = tag.get('style')
if style:
style = container.parse_css(style, is_declaration=True)
if remove_links_in_declaration(partial(container.href_to_name, base=name), style, predicate):
removed = True tag.set('style', css_text(style))
elif mt in OEB_STYLES:
removed = remove_links_in_sheet(partial(container.href_to_name, base=name), container.parsed(name), predicate)
if removed:
changed.add(name)
for i in changed:
container.dirty(i)
return changed def get_spine_order_for_all_files(container):
linear_names, non_linear_names = [], []
for name, is_linear in container.spine_names:
(linear_names if is_linear else non_linear_names).append(name)
all_names = linear_names + non_linear_names spine_names = frozenset(all_names)
ans = {}
for spine_pos, name in enumerate(all_names):
ans.setdefault(name, (spine_pos, -1))
for i, href in enumerate(container.iterlinks(name, get_line_numbers=False)):
lname = container.href_to_name(href, name)
if lname not in spine_names:
ans.setdefault(lname, (spine_pos, i))
return ans

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 시작 »
- 모듈 코드 »
- calibre.ebooks.oeb.polish.split

calibre.ebooks.oeb.polish.split의 소스 코드

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'

import copy import os import re from urllib.parse import urlparse from calibre.ebooks.oeb.base import OEB_DOCS, OPF, XHTML, XPNSMAP, XPath, barename from calibre.ebooks.oeb.polish.errors import MalformedMarkup from calibre.ebooks.oeb.polish.replace import LinkRebaser from calibre.ebooks.oeb.polish.toc import node_from_loc class AbortError(ValueError):
pass def in_table(node):
while node is not None:
if node.tag.endswith('}table'):
return True node = node.getparent()
return False def adjust_split_point(split_point, log):
'''
Move the split point up its ancestor chain if it has no content before it. This handles the common case:
<div id="chapter1"><h2>Chapter 1</h2>...</div> with a page break on the h2.
'''
sp = split_point while True:
parent = sp.getparent()
if ( parent is None or barename(parent.tag) in {'body', 'html'} or
(parent.text and parent.text.strip()) or parent.index(sp) > 0
):
break sp = parent if sp is not split_point:
log.debug('Adjusted split point to ancestor')

return sp def get_body(root):
return root.find('h:body', namespaces=XPNSMAP)

def do_split(split_point, log, before=True):
'''
Split tree into a *before* and an *after* tree at ``split_point``.

:param split_point: The Element at which to split
:param before: If True tree is split before split_point, otherwise after split_point
:return: before_tree, after_tree
'''
if before:
# We cannot adjust for after since moving an after split point to a
# parent will cause breakage if the parent contains any content
# after the original split point split_point = adjust_split_point(split_point, log)
tree = split_point.getroottree()
path = tree.getpath(split_point)

tree, tree2 = copy.deepcopy(tree), copy.deepcopy(tree)
root, root2 = tree.getroot(), tree2.getroot()
body, body2 = map(get_body, (root, root2))
split_point = root.xpath(path)[0]
split_point2 = root2.xpath(path)[0]

def nix_element(elem, top=True):
# Remove elem unless top is False in which case replace elem by its
# children parent = elem.getparent()
if top:
parent.remove(elem)
else:
index = parent.index(elem)
parent[index:index+1] = list(elem.iterchildren())

# Tree 1 hit_split_point = False keep_descendants = False split_point_descendants = frozenset(split_point.iterdescendants())
for elem in tuple(body.iterdescendants()):
if elem is split_point:
hit_split_point = True if before:
nix_element(elem)
else:
# We want to keep the descendants of the split point in
# Tree 1 keep_descendants = True
# We want the split point element, but not its tail elem.tail = '\n'

continue if hit_split_point:
if keep_descendants:
if elem in split_point_descendants:
# elem is a descendant keep it continue else:
# We are out of split_point, so prevent further set
# lookups of split_point_descendants keep_descendants = False nix_element(elem)

# Tree 2 ancestors = frozenset(XPath('ancestor::*')(split_point2))
for elem in tuple(body2.iterdescendants()):
if elem is split_point2:
if not before:
# Keep the split point element's tail, if it contains non-whitespace
# text tail = elem.tail if tail and not tail.isspace():
parent = elem.getparent()
idx = parent.index(elem)
if idx == 0:
parent.text = (parent.text or '') + tail else:
sib = parent[idx-1]
sib.tail = (sib.tail or '') + tail
# Remove the element itself nix_element(elem)
break if elem in ancestors:
# We have to preserve the ancestors as they could have CSS
# styles that are inherited/applicable, like font or
# width. So we only remove the text, if any.
elem.text = '\n'
else:
nix_element(elem, top=False)

body2.text = '\n'

return tree, tree2 class SplitLinkReplacer:

def __init__(self, base, bottom_anchors, top_name, bottom_name, container):
self.bottom_anchors, self.bottom_name = bottom_anchors, bottom_name self.container, self.top_name = container, top_name self.base = base self.replaced = False def __call__(self, url):
if url and url.startswith('#'):
return url name = self.container.href_to_name(url, self.base)
if name != self.top_name:
return url purl = urlparse(url)
if purl.fragment and purl.fragment in self.bottom_anchors:
url = self.container.name_to_href(self.bottom_name, self.base) + '#' + purl.fragment self.replaced = True return url

[문서]
def split(container, name, loc_or_xpath, before=True, totals=None):
'''
Split the file specified by name at the position specified by loc_or_xpath.
Splitting automatically migrates all links and references to the affected files.

:param loc_or_xpath: Should be an XPath expression such as
//h:div[@id="split_here"]. Can also be a *loc* which is used internally to implement splitting in the preview panel.
:param before: If True the split occurs before the identified element otherwise after it.
:param totals: Used internally
'''

root = container.parsed(name)
if isinstance(loc_or_xpath, str):
split_point = root.xpath(loc_or_xpath)[0]
else:
try:
split_point = node_from_loc(root, loc_or_xpath, totals=totals)
except MalformedMarkup:
# The webkit HTML parser and the container parser have yielded
# different node counts, this can happen if the file is valid XML
# but contains constructs like nested <p> tags. So force parse it
# with the HTML 5 parser and try again.
raw = container.raw_data(name)
root = container.parse_xhtml(raw, fname=name, force_html5_parse=True)
try:
split_point = node_from_loc(root, loc_or_xpath, totals=totals)
except MalformedMarkup:
raise MalformedMarkup(_('The file %s has malformed markup. Try running the Fix HTML tool'
' before splitting') % name)
container.replace(name, root)
if in_table(split_point):
raise AbortError('Cannot split inside tables')
if split_point.tag.endswith('}body'):
raise AbortError('Cannot split on the <body> tag')
tree1, tree2 = do_split(split_point, container.log, before=before)
root1, root2 = tree1.getroot(), tree2.getroot()
anchors_in_top = frozenset(root1.xpath('//*/@id')) | frozenset(root1.xpath('//*/@name')) | {''}
anchors_in_bottom = frozenset(root2.xpath('//*/@id')) | frozenset(root2.xpath('//*/@name'))
base, ext = name.rpartition('.')[0::2]
base = re.sub(r'_split\d+$', '', base)
nname, s = None, 0 while not nname or container.exists(nname):
s += 1 nname = f'{base}_split{s}.{ext}'
manifest_item = container.generate_item(nname, media_type=container.mime_map[name])
bottom_name = container.href_to_name(manifest_item.get('href'), container.opf_name)

# Fix links in the split trees for r in (root1, root2):
for a in r.xpath('//*[@href]'):
url = a.get('href')
if url.startswith('#'):
fname = name else:
fname = container.href_to_name(url, name)
if fname == name:
purl = urlparse(url)
if purl.fragment in anchors_in_top:
if r is root2:
a.set('href', f'{container.name_to_href(name, bottom_name)}#{purl.fragment}')
else:
a.set('href', '#' + purl.fragment)
elif purl.fragment in anchors_in_bottom:
if r is root1:
a.set('href', f'{container.name_to_href(bottom_name, name)}#{purl.fragment}')
else:
a.set('href', '#' + purl.fragment)

# Fix all links in the container that point to anchors in the bottom tree for fname, media_type in container.mime_map.items():
if fname not in {name, bottom_name}:
repl = SplitLinkReplacer(fname, anchors_in_bottom, name, bottom_name, container)
container.replace_links(fname, repl)

container.replace(name, root1)
container.replace(bottom_name, root2)

spine = container.opf_xpath('//opf:spine')[0]
for spine_item, spine_name, linear in container.spine_iter:
if spine_name == name:
break index = spine.index(spine_item) + 1 si = spine.makeelement(OPF('itemref'), idref=manifest_item.get('id'))
if not linear:
si.set('linear', 'no')
container.insert_into_xml(spine, si, index=index)
container.dirty(container.opf_name)
return bottom_name

[문서]
def multisplit(container, name, xpath, before=True):
'''
Split the specified file at multiple locations (all tags that match the specified XPath expression). See also: :func:`split`.
Splitting automatically migrates all links and references to the affected files.

:param before: If True the splits occur before the identified element otherwise after it.
'''
root = container.parsed(name)
nodes = root.xpath(xpath, namespaces=XPNSMAP)
if not nodes:
raise AbortError(_('The expression %s did not match any nodes') % xpath)
for split_point in nodes:
if in_table(split_point):
raise AbortError('Cannot split inside tables')
if split_point.tag.endswith('}body'):
raise AbortError('Cannot split on the <body> tag')

for i, tag in enumerate(nodes):
tag.set('calibre-split-point', str(i))

current = name all_names = [name]
for i in range(len(nodes)):
current = split(container, current, f'//*[@calibre-split-point="{i}"]', before=before)
all_names.append(current)

for x in all_names:
for tag in container.parsed(x).xpath('//*[@calibre-split-point]'):
tag.attrib.pop('calibre-split-point')
container.dirty(x)

return all_names[1:]

class MergeLinkReplacer:

def __init__(self, base, anchor_map, master, container):
self.container, self.anchor_map = container, anchor_map self.master = master self.base = base self.replaced = False def __call__(self, url):
if url and url.startswith('#'):
return url name = self.container.href_to_name(url, self.base)
amap = self.anchor_map.get(name, None)
if amap is None:
return url purl = urlparse(url)
frag = purl.fragment or ''
frag = amap.get(frag, frag)
url = self.container.name_to_href(self.master, self.base) + '#' + frag self.replaced = True return url def add_text(body, text):
if len(body) > 0:
body[-1].tail = (body[-1].tail or '') + text else:
body.text = (body.text or '') + text def all_anchors(root):
return set(root.xpath('//*/@id')) | set(root.xpath('//*/@name'))

def all_stylesheets(container, name):
for link in XPath('//h:head/h:link[@href]')(container.parsed(name)):
name = container.href_to_name(link.get('href'), name)
typ = link.get('type', 'text/css')
if typ == 'text/css':
yield name def unique_anchor(seen_anchors, current):
c = 0 ans = current while ans in seen_anchors:
c += 1 ans = f'{current}_{c}'
return ans def remove_name_attributes(root):
# Remove all name attributes, replacing them with id attributes for elem in root.xpath('//*[@id and @name]'):
del elem.attrib['name']
for elem in root.xpath('//*[@name]'):
elem.set('id', elem.attrib.pop('name'))

def merge_html(container, names, master, insert_page_breaks=False):
p = container.parsed root = p(master)

# Ensure master has a <head> head = root.find('h:head', namespaces=XPNSMAP)
if head is None:
head = root.makeelement(XHTML('head'))
container.insert_into_xml(root, head, 0)

seen_anchors = all_anchors(root)
seen_stylesheets = set(all_stylesheets(container, master))
master_body = p(master).findall('h:body', namespaces=XPNSMAP)[-1]
master_base = os.path.dirname(master)
anchor_map = {n:{} for n in names if n != master}
first_anchor_map = {}

for name in names:
if name == master:
continue
# Insert new stylesheets into master for sheet in all_stylesheets(container, name):
if sheet not in seen_stylesheets:
seen_stylesheets.add(sheet)
link = head.makeelement(XHTML('link'), rel='stylesheet', type='text/css', href=container.name_to_href(sheet, master))
container.insert_into_xml(head, link)

# Rebase links if master is in a different directory if os.path.dirname(name) != master_base:
container.replace_links(name, LinkRebaser(container, name, master))

root = p(name)
children = []
for body in p(name).findall('h:body', namespaces=XPNSMAP):
children.append(body.text if body.text and body.text.strip() else '\n\n')
children.extend(body)

first_child = ''
for first_child in children:
if not isinstance(first_child, (str, bytes)):
break if isinstance(first_child, (str, bytes)):
# body contained only text, no tags first_child = body.makeelement(XHTML('p'))
first_child.text, children[0] = children[0], first_child amap = anchor_map[name]
remove_name_attributes(root)

for elem in root.xpath('//*[@id]'):
val = elem.get('id')
if not val:
continue if val in seen_anchors:
nval = unique_anchor(seen_anchors, val)
elem.set('id', nval)
amap[val] = nval else:
seen_anchors.add(val)

if 'id' not in first_child.attrib:
first_child.set('id', unique_anchor(seen_anchors, 'top'))
seen_anchors.add(first_child.get('id'))
first_anchor_map[name] = first_child.get('id')

if insert_page_breaks:
first_child.set('style', first_child.get('style', '') + '; page-break-before: always')

amap[''] = first_child.get('id')

# Fix links that point to local changed anchors for a in XPath('//h:a[starts-with(@href, "#")]')(root):
q = a.get('href')[1:]
if q in amap:
a.set('href', '#' + amap[q])

for child in children:
if isinstance(child, (str, bytes)):
add_text(master_body, child)
else:
master_body.append(copy.deepcopy(child))

container.remove_item(name, remove_from_guide=False)

# Fix all links in the container that point to merged files for fname, media_type in container.mime_map.items():
repl = MergeLinkReplacer(fname, anchor_map, master, container)
container.replace_links(fname, repl)

return first_anchor_map def merge_css(container, names, master):
p = container.parsed msheet = p(master)
master_base = os.path.dirname(master)
merged = set()

for name in names:
if name == master:
continue
# Rebase links if master is in a different directory if os.path.dirname(name) != master_base:
container.replace_links(name, LinkRebaser(container, name, master))

sheet = p(name)

# Remove charset rules cr = [r for r in sheet.cssRules if r.type == r.CHARSET_RULE]
[sheet.deleteRule(sheet.cssRules.index(r)) for r in cr]
for rule in sheet.cssRules:
msheet.add(rule)

container.remove_item(name)
merged.add(name)

# Remove links to merged stylesheets in the html files, replacing with a
# link to the master sheet for name, mt in container.mime_map.items():
if mt in OEB_DOCS:
removed = False root = p(name)
for link in XPath('//h:link[@href]')(root):
q = container.href_to_name(link.get('href'), name)
if q in merged:
container.remove_from_xml(link)
removed = True if removed:
container.dirty(name)
if removed and master not in set(all_stylesheets(container, name)):
head = root.find('h:head', namespaces=XPNSMAP)
if head is not None:
link = head.makeelement(XHTML('link'), type='text/css', rel='stylesheet', href=container.name_to_href(master, name))
container.insert_into_xml(head, link)

[문서]
def merge(container, category, names, master):
'''
Merge the specified files into a single file, automatically migrating all links and references to the affected files. The file must all either be HTML or CSS files.

:param category: Must be either ``'text'`` for HTML files or ``'styles'`` for CSS files
:param names: The list of files to be merged
:param master: Which of the merged files is the *master* file, that is, the file that will remain after merging.
'''
if category not in {'text', 'styles'}:
raise AbortError(f'Cannot merge files of type: {category}')
if len(names) < 2:
raise AbortError('Must specify at least two files to be merged')
if master not in names:
raise AbortError(f'The master file ({master}) must be one of the files being merged')

if category == 'text':
merge_html(container, names, master)
elif category == 'styles':
merge_css(container, names, master)

container.dirty(master)

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 시작 »
- 모듈 코드 »
- calibre.ebooks.oeb.polish.toc

calibre.ebooks.oeb.polish.toc의 소스 코드

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import re from collections import Counter, OrderedDict from functools import partial from operator import itemgetter from urllib.parse import urlparse from lxml import etree from lxml.builder import ElementMaker from calibre import __version__ from calibre.ebooks.oeb.base import EPUB_NS, NCX, NCX_NS, OEB_DOCS, XHTML, XHTML_NS, XML, XML_NS, XPath, serialize, uuid_id, xml2text from calibre.ebooks.oeb.polish.errors import MalformedMarkup from calibre.ebooks.oeb.polish.opf import get_book_language, set_guide_item from calibre.ebooks.oeb.polish.pretty import pretty_html_tree, pretty_xml_tree from calibre.ebooks.oeb.polish.utils import extract, guess_type from calibre.translations.dynamic import translate from calibre.utils.localization import canonicalize_lang, get_lang, lang_as_iso639_1 from calibre.utils.resources import get_path as P ns = etree.FunctionNamespace('calibre_xpath_extensions')
ns.prefix = 'calibre'
ns['lower-case'] = lambda c, x: x.lower() if hasattr(x, 'lower') else x class TOC:

toc_title = None def __init__(self, title=None, dest=None, frag=None):
self.title, self.dest, self.frag = title, dest, frag self.dest_exists = self.dest_error = None if self.title:
self.title = self.title.strip()
self.parent = None self.children = []
self.page_list = []

def add(self, title, dest, frag=None):
c = TOC(title, dest, frag)
self.children.append(c)
c.parent = self return c def remove(self, child):
self.children.remove(child)
child.parent = None def remove_from_parent(self):
if self.parent is None:
return idx = self.parent.children.index(self)
for child in reversed(self.children):
child.parent = self.parent self.parent.children.insert(idx, child)
self.parent.children.remove(self)
self.parent = None def __iter__(self):
yield from self.children def __len__(self):
return len(self.children)

def iterdescendants(self, level=None):
gc_level = None if level is None else level + 1 for child in self:
if level is None:
yield child else:
yield level, child yield from child.iterdescendants(level=gc_level)

def remove_duplicates(self, only_text=True):
seen = set()
remove = []
for child in self:
key = child.title if only_text else (child.title, child.dest, (child.frag or None))
if key in seen:
remove.append(child)
else:
seen.add(key)
child.remove_duplicates()
for child in remove:
self.remove(child)

@property def depth(self):
'''The maximum depth of the navigation tree rooted at this node.'''
try:
return max(node.depth for node in self) + 1 except ValueError:
return 1

@property def last_child(self):
return self.children[-1] if self.children else None def get_lines(self, lvl=0):
frag = ('#'+self.frag) if self.frag else ''
ans = [('\t'*lvl) + f'TOC: {self.title} --> {self.dest}{frag}']
for child in self:
ans.extend(child.get_lines(lvl+1))
return ans def __str__(self):
return '\n'.join(self.get_lines())

def to_dict(self, node_counter=None):
ans = {
'title':self.title, 'dest':self.dest, 'frag':self.frag,
'children':[c.to_dict(node_counter) for c in self.children]
}
if self.dest_exists is not None:
ans['dest_exists'] = self.dest_exists if self.dest_error is not None:
ans['dest_error'] = self.dest_error if node_counter is not None:
ans['id'] = next(node_counter)
return ans

@property def as_dict(self):
return self.to_dict()

def child_xpath(tag, name):
return tag.xpath(f'./*[calibre:lower-case(local-name()) = "{name}"]')

def add_from_navpoint(container, navpoint, parent, ncx_name):
dest = frag = text = None nl = child_xpath(navpoint, 'navlabel')
if nl:
nl = nl[0]
text = ''
for txt in child_xpath(nl, 'text'):
text += etree.tostring(txt, method='text', encoding='unicode', with_tail=False)
content = child_xpath(navpoint, 'content')
if content:
content = content[0]
href = content.get('src', None)
if href:
dest = container.href_to_name(href, base=ncx_name)
frag = urlparse(href).fragment or None return parent.add(text or None, dest or None, frag or None)

def process_ncx_node(container, node, toc_parent, ncx_name):
for navpoint in node.xpath('./*[calibre:lower-case(local-name()) = "navpoint"]'):
child = add_from_navpoint(container, navpoint, toc_parent, ncx_name)
if child is not None:
process_ncx_node(container, navpoint, child, ncx_name)

def parse_ncx(container, ncx_name):
root = container.parsed(ncx_name)
toc_root = TOC()
navmaps = root.xpath('//*[calibre:lower-case(local-name()) = "navmap"]')
if navmaps:
process_ncx_node(container, navmaps[0], toc_root, ncx_name)
toc_root.lang = toc_root.uid = None for attr, val in root.attrib.items():
if attr.endswith('lang'):
toc_root.lang = str(val)
break for uid in root.xpath('//*[calibre:lower-case(local-name()) = "meta" and @name="dtb:uid"]/@content'):
if uid:
toc_root.uid = str(uid)
break for pl in root.xpath('//*[calibre:lower-case(local-name()) = "pagelist"]'):
for pt in pl.xpath('descendant::*[calibre:lower-case(local-name()) = "pagetarget"]'):
pagenum = pt.get('value')
if pagenum:
href = pt.xpath('descendant::*[calibre:lower-case(local-name()) = "content"]/@src')
if href:
dest = container.href_to_name(href[0], base=ncx_name)
frag = urlparse(href[0]).fragment or None toc_root.page_list.append({'dest': dest, 'pagenum': pagenum, 'frag': frag})
return toc_root def add_from_li(container, li, parent, nav_name):
dest = frag = text = None for x in li.iterchildren(XHTML('a'), XHTML('span')):
text = etree.tostring(x, method='text', encoding='unicode', with_tail=False).strip() or ' '.join(x.xpath('descendant-or-self::*/@title')).strip()
href = x.get('href')
if href:
dest = nav_name if href.startswith('#') else container.href_to_name(href, base=nav_name)
frag = urlparse(href).fragment or None break return parent.add(text or None, dest or None, frag or None)

def first_child(parent, tagname):
try:
return next(parent.iterchildren(tagname))
except StopIteration:
return None def process_nav_node(container, node, toc_parent, nav_name):
for li in node.iterchildren(XHTML('li')):
child = add_from_li(container, li, toc_parent, nav_name)
ol = first_child(li, XHTML('ol'))
if child is not None and ol is not None:
process_nav_node(container, ol, child, nav_name)

def parse_nav(container, nav_name):
root = container.parsed(nav_name)
toc_root = TOC()
toc_root.lang = toc_root.uid = None seen_toc = seen_pagelist = False et = f'{{{EPUB_NS}}}type'
for nav in XPath('descendant::h:nav[@epub:type]')(root):
nt = nav.get(et)
if nt == 'toc' and not seen_toc:
ol = first_child(nav, XHTML('ol'))
if ol is not None:
seen_toc = True process_nav_node(container, ol, toc_root, nav_name)
for h in nav.iterchildren(*map(XHTML, 'h1 h2 h3 h4 h5 h6'.split())):
text = etree.tostring(h, method='text', encoding='unicode', with_tail=False) or h.get('title')
if text:
toc_root.toc_title = text break elif nt == 'page-list' and not seen_pagelist:
ol = first_child(nav, XHTML('ol'))
if ol is not None and not seen_pagelist:
seen_pagelist = True for li in ol.iterchildren(XHTML('li')):
for a in li.iterchildren(XHTML('a')):
href = a.get('href')
if href:
text = (etree.tostring(a, method='text', encoding='unicode', with_tail=False) or a.get('title')).strip()
if text:
dest = nav_name if href.startswith('#') else container.href_to_name(href, base=nav_name)
frag = urlparse(href).fragment or None toc_root.page_list.append({'dest': dest, 'pagenum': text, 'frag': frag})
return toc_root def verify_toc_destinations(container, toc):
anchor_map = {}
anchor_xpath = XPath('//*/@id|//h:a/@name')
for item in toc.iterdescendants():
name = item.dest if not name:
item.dest_exists = False item.dest_error = _('No file named %s exists')%name continue try:
root = container.parsed(name)
except KeyError:
item.dest_exists = False item.dest_error = _('No file named %s exists')%name continue if not hasattr(root, 'xpath'):
item.dest_exists = False item.dest_error = _('No HTML file named %s exists')%name continue if not item.frag:
item.dest_exists = True continue if name not in anchor_map:
anchor_map[name] = frozenset(anchor_xpath(root))
item.dest_exists = item.frag in anchor_map[name]
if not item.dest_exists:
item.dest_error = _(
'The anchor %(a)s does not exist in file %(f)s')%dict( a=item.frag, f=name)

def find_existing_ncx_toc(container):
toc = container.opf_xpath('//opf:spine/@toc')
if toc:
toc = container.manifest_id_map.get(toc[0], None)
if not toc:
ncx = guess_type('a.ncx')
toc = container.manifest_type_map.get(ncx, [None])[0]
return toc or None def find_existing_nav_toc(container):
for name in container.manifest_items_with_property('nav'):
return name def mark_as_nav(container, name):
if container.opf_version_parsed.major > 2:
container.apply_unique_properties(name, 'nav')

def get_x_toc(container, find_toc, parse_toc, verify_destinations=True):
def empty_toc():
ans = TOC()
ans.lang = ans.uid = None return ans toc = find_toc(container)
ans = empty_toc() if toc is None or not container.has_name(toc) else parse_toc(container, toc)
ans.toc_file_name = toc if toc and container.has_name(toc) else None if verify_destinations:
verify_toc_destinations(container, ans)
return ans def get_toc(container, verify_destinations=True):
ver = container.opf_version_parsed if ver.major < 3:
return get_x_toc(container, find_existing_ncx_toc, parse_ncx, verify_destinations=verify_destinations)
else:
ans = get_x_toc(container, find_existing_nav_toc, parse_nav, verify_destinations=verify_destinations)
if len(ans) == 0:
ans = get_x_toc(container, find_existing_ncx_toc, parse_ncx, verify_destinations=verify_destinations)
return ans def get_guide_landmarks(container):
for ref in container.opf_xpath('./opf:guide/opf:reference'):
href, title, rtype = ref.get('href'), ref.get('title'), ref.get('type')
href, frag = href.partition('#')[::2]
name = container.href_to_name(href, container.opf_name)
if container.has_name(name):
yield {'dest':name, 'frag':frag, 'title':title or '', 'type':rtype or ''}

def get_nav_landmarks(container):
nav = find_existing_nav_toc(container)
if nav and container.has_name(nav):
root = container.parsed(nav)
et = f'{{{EPUB_NS}}}type'
for elem in root.iterdescendants(XHTML('nav')):
if elem.get(et) == 'landmarks':
for li in elem.iterdescendants(XHTML('li')):
for a in li.iterdescendants(XHTML('a')):
href, rtype = a.get('href'), a.get(et)
if href:
title = etree.tostring(a, method='text', encoding='unicode', with_tail=False).strip()
href, frag = href.partition('#')[::2]
name = container.href_to_name(href, nav)
if container.has_name(name):
yield {'dest':name, 'frag':frag, 'title':title or '', 'type':rtype or ''}
break def get_landmarks(container):
ver = container.opf_version_parsed if ver.major < 3:
return list(get_guide_landmarks(container))
ans = list(get_nav_landmarks(container))
if len(ans) == 0:
ans = list(get_guide_landmarks(container))
return ans def ensure_id(elem, all_ids):
elem_id = elem.get('id')
if elem_id:
return False, elem_id if elem.tag == XHTML('a'):
anchor = elem.get('name', None)
if anchor:
elem.set('id', anchor)
return False, anchor c = 0 while True:
c += 1 q = f'toc_{c}'
if q not in all_ids:
elem.set('id', q)
all_ids.add(q)
break return True, elem.get('id')

def elem_to_toc_text(elem, prefer_title=False):
text = xml2text(elem).strip()
if prefer_title:
text = elem.get('title', '').strip() or text if not text:
text = elem.get('title', '')
if not text:
text = elem.get('alt', '')
text = re.sub(r'\s+', ' ', text.strip())
text = text[:1000].strip()
if not text:
text = _('(Untitled)')
return text def item_at_top(elem):
try:
body = XPath('//h:body')(elem.getroottree().getroot())[0]
except (TypeError, IndexError, KeyError, AttributeError):
return False tree = body.getroottree()
path = tree.getpath(elem)
for el in body.iterdescendants(etree.Element):
epath = tree.getpath(el)
if epath == path:
break try:
if el.tag.endswith('}img') or (el.text and el.text.strip()):
return False except Exception:
return False if not path.startswith(epath):
# Only check tail of non-parent elements if el.tail and el.tail.strip():
return False return True

[문서]
def from_xpaths(container, xpaths, prefer_title=False):
'''
Generate a Table of Contents from a list of XPath expressions. Each expression in the list corresponds to a level of the generate ToC. For example: :code:`['//h:h1', '//h:h2', '//h:h3']` will generate a three level
Table of Contents from the ``<h1>``, ``<h2>`` and ``<h3>`` tags.
'''
tocroot = TOC()
xpaths = [XPath(xp) for xp in xpaths]

# Find those levels that have no elements in all spine items maps = OrderedDict()
empty_levels = {i+1 for i, xp in enumerate(xpaths)}
for spinepath in container.spine_items:
name = container.abspath_to_name(spinepath)
root = container.parsed(name)
level_item_map = maps[name] = {i+1:frozenset(xp(root)) for i, xp in enumerate(xpaths)}
for lvl, elems in level_item_map.items():
if elems:
empty_levels.discard(lvl)
# Remove empty levels from all level_maps if empty_levels:
for name, lmap in tuple(maps.items()):
lmap = {lvl:items for lvl, items in lmap.items() if lvl not in empty_levels}
lmap = sorted(lmap.items(), key=itemgetter(0))
lmap = {i+1:items for i, (l, items) in enumerate(lmap)}
maps[name] = lmap node_level_map = {tocroot: 0}

def parent_for_level(child_level):
limit = child_level - 1 def process_node(node):
child = node.last_child if child is None:
return node lvl = node_level_map[child]
return node if lvl > limit else child if lvl == limit else process_node(child)

return process_node(tocroot)

for name, level_item_map in maps.items():
root = container.parsed(name)
item_level_map = {e:i for i, elems in level_item_map.items() for e in elems}
item_dirtied = False all_ids = set(root.xpath('//*/@id'))

for item in root.iterdescendants(etree.Element):
lvl = item_level_map.get(item, None)
if lvl is None:
continue text = elem_to_toc_text(item, prefer_title)
parent = parent_for_level(lvl)
if item_at_top(item):
dirtied, elem_id = False, None else:
dirtied, elem_id = ensure_id(item, all_ids)
item_dirtied = dirtied or item_dirtied toc = parent.add(text, name, elem_id)
node_level_map[toc] = lvl toc.dest_exists = True if item_dirtied:
container.commit_item(name, keep_parsed=True)

return tocroot[문서]
def from_links(container):
'''
Generate a Table of Contents from links in the book.
'''
toc = TOC()
link_path = XPath('//h:a[@href]')
seen_titles, seen_dests = set(), set()
for name, is_linear in container.spine_names:
root = container.parsed(name)
for a in link_path(root):
href = a.get('href')
if not href or not href.strip():
continue frag = None if href.startswith('#'):
dest = name frag = href[1:]
else:
href, _, frag = href.partition('#')
dest = container.href_to_name(href, base=name)
frag = frag or None if (dest, frag) in seen_dests:
continue seen_dests.add((dest, frag))
text = elem_to_toc_text(a)
if text in seen_titles:
continue seen_titles.add(text)
toc.add(text, dest, frag=frag)
verify_toc_destinations(container, toc)
for child in toc:
if not child.dest_exists:
toc.remove(child)
return tocdef find_text(node):
LIMIT = 200 pat = re.compile(r'\s+')
for child in node:
if isinstance(child, etree._Element):
text = xml2text(child).strip()
text = pat.sub(' ', text)
if len(text) < 1:
continue if len(text) > LIMIT:
# Look for less text in a child of this node, recursively ntext = find_text(child)
return ntext or (text[:LIMIT] + '...')
else:
return text

[문서]
def from_files(container):
'''
Generate a Table of Contents from files in the book.
'''
toc = TOC()
for i, spinepath in enumerate(container.spine_items):
name = container.abspath_to_name(spinepath)
root = container.parsed(name)
body = XPath('//h:body')(root)
if not body:
continue text = find_text(body[0])
if not text:
text = name.rpartition('/')[-1]
if i == 0 and text.rpartition('.')[0].lower() in {'titlepage', 'cover'}:
text = _('Cover')
toc.add(text, name)
return toc
def node_from_loc(root, locs, totals=None):
node = root.xpath('//*[local-name()="body"]')[0]
for i, loc in enumerate(locs):
children = tuple(node.iterchildren(etree.Element))
if totals is not None and totals[i] != len(children):
raise MalformedMarkup()
node = children[loc]
return node def add_id(container, name, loc, totals=None):
root = container.parsed(name)
try:
node = node_from_loc(root, loc, totals=totals)
except MalformedMarkup:
# The webkit HTML parser and the container parser have yielded
# different node counts, this can happen if the file is valid XML
# but contains constructs like nested <p> tags. So force parse it
# with the HTML 5 parser and try again.
raw = container.raw_data(name)
root = container.parse_xhtml(raw, fname=name, force_html5_parse=True)
try:
node = node_from_loc(root, loc, totals=totals)
except MalformedMarkup:
raise MalformedMarkup(_('The file %s has malformed markup. Try running the Fix HTML tool'
' before editing.') % name)
container.replace(name, root)

if not node.get('id'):
ensure_id(node, set(root.xpath('//*/@id')))
container.commit_item(name, keep_parsed=True)
return node.get('id')

def create_ncx(toc, to_href, btitle, lang, uid):
lang = lang.replace('_', '-')
ncx = etree.Element(NCX('ncx'), attrib={'version': '2005-1', XML('lang'): lang}, nsmap={None: NCX_NS})
head = etree.SubElement(ncx, NCX('head'))
etree.SubElement(head, NCX('meta'), name='dtb:uid', content=str(uid))
etree.SubElement(head, NCX('meta'), name='dtb:depth', content=str(toc.depth))
generator = ''.join(['calibre (', __version__, ')'])
etree.SubElement(head, NCX('meta'), name='dtb:generator', content=generator)
etree.SubElement(head, NCX('meta'), name='dtb:totalPageCount', content='0')
etree.SubElement(head, NCX('meta'), name='dtb:maxPageNumber', content='0')
title = etree.SubElement(ncx, NCX('docTitle'))
text = etree.SubElement(title, NCX('text'))
text.text = btitle navmap = etree.SubElement(ncx, NCX('navMap'))
spat = re.compile(r'\s+')

play_order = Counter()

def process_node(xml_parent, toc_parent):
for child in toc_parent:
play_order['c'] += 1 point = etree.SubElement(xml_parent, NCX('navPoint'), id=f"num_{play_order['c']}", playOrder=str(play_order['c']))
label = etree.SubElement(point, NCX('navLabel'))
title = child.title if title:
title = spat.sub(' ', title)
etree.SubElement(label, NCX('text')).text = title if child.dest:
href = to_href(child.dest)
if child.frag:
href += '#'+child.frag etree.SubElement(point, NCX('content'), src=href)
process_node(point, child)

process_node(navmap, toc)
return ncx def commit_ncx_toc(container, toc, lang=None, uid=None):
tocname = find_existing_ncx_toc(container)
if tocname is None:
item = container.generate_item('toc.ncx', id_prefix='toc')
tocname = container.href_to_name(item.get('href'), base=container.opf_name)
ncx_id = item.get('id')
[s.set('toc', ncx_id) for s in container.opf_xpath('//opf:spine')]
if not lang:
lang = get_lang()
for l in container.opf_xpath('//dc:language'):
l = canonicalize_lang(xml2text(l).strip())
if l:
lang = l lang = lang_as_iso639_1(l) or l break lang = lang_as_iso639_1(lang) or lang if not uid:
uid = uuid_id()
eid = container.opf.get('unique-identifier', None)
if eid:
m = container.opf_xpath(f'//*[@id="{eid}"]')
if m:
uid = xml2text(m[0])

title = _('Table of Contents')
m = container.opf_xpath('//dc:title')
if m:
x = xml2text(m[0]).strip()
title = x or title to_href = partial(container.name_to_href, base=tocname)
root = create_ncx(toc, to_href, title, lang, uid)
container.replace(tocname, root)
container.pretty_print.add(tocname)

def ensure_single_nav_of_type(root, ntype='toc'):
et = f'{{{EPUB_NS}}}type'
navs = [n for n in root.iterdescendants(XHTML('nav')) if n.get(et) == ntype]
for x in navs[1:]:
extract(x)
if navs:
nav = navs[0]
tail = nav.tail attrib = dict(nav.attrib)
nav.clear()
nav.attrib.update(attrib)
nav.tail = tail else:
nav = root.makeelement(XHTML('nav'))
first_child(root, XHTML('body')).append(nav)
nav.set(f'{{{EPUB_NS}}}type', ntype)
return nav def ensure_container_has_nav(container, lang=None, previous_nav=None):
tocname = find_existing_nav_toc(container)
if previous_nav is not None:
nav_name = container.href_to_name(previous_nav[0])
if nav_name and container.exists(nav_name):
tocname = nav_name container.apply_unique_properties(tocname, 'nav')
if tocname is None:
name = previous_nav[0] if previous_nav is not None else 'nav.xhtml'
item = container.generate_item(name, id_prefix='nav')
item.set('properties', 'nav')
tocname = container.href_to_name(item.get('href'), base=container.opf_name)
if previous_nav is not None:
root = previous_nav[1]
else:
root = container.parse_xhtml(P('templates/new_nav.html', data=True).decode('utf-8'))
container.replace(tocname, root)
else:
root = container.parsed(tocname)
if lang:
lang = lang_as_iso639_1(lang) or lang root.set('lang', lang)
root.set(f'{{{XML_NS}}}lang', lang)
return tocname, root def collapse_li(parent):
for li in parent.iterdescendants(XHTML('li')):
if len(li) == 1:
li.text = None li[0].tail = None def create_nav_li(container, ol, entry, tocname):
li = ol.makeelement(XHTML('li'))
ol.append(li)
a = li.makeelement(XHTML('a'))
li.append(a)
href = container.name_to_href(entry['dest'], tocname)
if entry['frag']:
href += '#' + entry['frag']
a.set('href', href)
return a def set_landmarks(container, root, tocname, landmarks):
nav = ensure_single_nav_of_type(root, 'landmarks')
nav.set('hidden', '')
ol = nav.makeelement(XHTML('ol'))
nav.append(ol)
for entry in landmarks:
if entry['type'] and container.has_name(entry['dest']) and container.mime_map[entry['dest']] in OEB_DOCS:
a = create_nav_li(container, ol, entry, tocname)
a.set(f'{{{EPUB_NS}}}type', entry['type'])
a.text = entry['title'] or None pretty_xml_tree(nav)
collapse_li(nav)

def commit_nav_toc(container, toc, lang=None, landmarks=None, previous_nav=None):
tocname, root = ensure_container_has_nav(container, lang=lang, previous_nav=previous_nav)
nav = ensure_single_nav_of_type(root, 'toc')
if toc.toc_title:
nav.append(nav.makeelement(XHTML('h1')))
nav[-1].text = toc.toc_title rnode = nav.makeelement(XHTML('ol'))
nav.append(rnode)
to_href = partial(container.name_to_href, base=tocname)
spat = re.compile(r'\s+')

def process_node(xml_parent, toc_parent):
for child in toc_parent:
li = xml_parent.makeelement(XHTML('li'))
xml_parent.append(li)
title = child.title or ''
title = spat.sub(' ', title).strip()
a = li.makeelement(XHTML('a' if child.dest else 'span'))
a.text = title li.append(a)
if child.dest:
href = to_href(child.dest)
if child.frag:
href += '#'+child.frag a.set('href', href)
if len(child):
ol = li.makeelement(XHTML('ol'))
li.append(ol)
process_node(ol, child)
process_node(rnode, toc)
pretty_xml_tree(nav)

collapse_li(nav)
nav.tail = '\n'

if toc.page_list:
nav = ensure_single_nav_of_type(root, 'page-list')
nav.set('hidden', '')
ol = nav.makeelement(XHTML('ol'))
nav.append(ol)
for entry in toc.page_list:
if container.has_name(entry['dest']) and container.mime_map[entry['dest']] in OEB_DOCS:
a = create_nav_li(container, ol, entry, tocname)
a.text = str(entry['pagenum'])
pretty_xml_tree(nav)
collapse_li(nav)
container.replace(tocname, root)

def commit_toc(container, toc, lang=None, uid=None):
commit_ncx_toc(container, toc, lang=lang, uid=uid)
if container.opf_version_parsed.major > 2:
commit_nav_toc(container, toc, lang=lang)

def remove_names_from_toc(container, names):
changed = []
names = frozenset(names)
for find_toc, parse_toc, commit_toc in (
(find_existing_ncx_toc, parse_ncx, commit_ncx_toc),
(find_existing_nav_toc, parse_nav, commit_nav_toc),
):
toc = get_x_toc(container, find_toc, parse_toc, verify_destinations=False)
if len(toc) > 0:
remove = []
for node in toc.iterdescendants():
if node.dest in names:
remove.append(node)
if remove:
for node in reversed(remove):
node.remove_from_parent()
commit_toc(container, toc)
changed.append(find_toc(container))
return changed def find_inline_toc(container):
for name, linear in container.spine_names:
if container.parsed(name).xpath('//*[local-name()="body" and @id="calibre_generated_inline_toc"]'):
return name def toc_to_html(toc, container, toc_name, title, lang=None):

def process_node(html_parent, toc, level=1, indent=' ', style_level=2):
li = html_parent.makeelement(XHTML('li'))
li.tail = '\n'+ (indent*level)
html_parent.append(li)
name, frag = toc.dest, toc.frag href = '#'
if name:
href = container.name_to_href(name, toc_name)
if frag:
href += '#' + frag a = li.makeelement(XHTML('a'), href=href)
a.text = toc.title li.append(a)
if len(toc) > 0:
parent = li.makeelement(XHTML('ul'))
parent.set('class', f'level{style_level}')
li.append(parent)
a.tail = '\n\n' + (indent*(level+2))
parent.text = '\n'+(indent*(level+3))
parent.tail = '\n\n' + (indent*(level+1))
for child in toc:
process_node(parent, child, level+3, style_level=style_level + 1)
parent[-1].tail = '\n' + (indent*(level+2))

E = ElementMaker(namespace=XHTML_NS, nsmap={None:XHTML_NS})
html = E.html(
E.head(
E.title(title),
E.style(P('templates/inline_toc_styles.css', data=True).decode('utf-8'), type='text/css'),
),
E.body(
E.h2(title),
E.ul(), id='calibre_generated_inline_toc',
)
)

ul = html[1][1]
ul.set('class', 'level1')
for child in toc:
process_node(ul, child)
if lang:
html.set('lang', lang)
pretty_html_tree(container, html)
return html

[문서]
def create_inline_toc(container, title=None):
'''
Create an inline (HTML) Table of Contents from an existing NCX Table of Contents.

:param title: The title for this table of contents.
'''
lang = get_book_language(container)
default_title = 'Table of Contents'
if lang:
lang = lang_as_iso639_1(lang) or lang default_title = translate(lang, default_title)
title = title or default_title toc = get_toc(container)
if len(toc) == 0:
return None toc_name = find_inline_toc(container)

name = toc_name html = toc_to_html(toc, container, name, title, lang)
raw = serialize(html, 'text/html')
if name is None:
name, c = 'toc.xhtml', 0 while container.has_name(name):
c += 1 name = f'toc{c}.xhtml'
container.add_file(name, raw, spine_index=0)
else:
with container.open(name, 'wb') as f:
f.write(raw)
set_guide_item(container, 'toc', title, name, frag='calibre_generated_inline_toc')
return name

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 시작 »
- 모듈 코드 »
- calibre.gui2.actions

calibre.gui2.actions의 소스 코드

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

from functools import partial from zipfile import ZipFile from qt.core import QAction, QIcon, QKeySequence, QMenu, QObject, QPoint, QTimer, QToolButton from calibre import prints from calibre.constants import ismacos from calibre.gui2 import Dispatcher from calibre.gui2.keyboard import NameConflict def toolbar_widgets_for_action(gui, action):
# Search the toolbars for the widget associated with an action, passing
# them to the caller for further processing for x in gui.bars_manager.bars:
try:
w = x.widgetForAction(action)
# It seems that multiple copies of the action can exist, such as
# when the device-connected menu is changed while the device is
# connected. Use the one that has an actual position.
if w is None or w.pos().x() == 0:
continue
# The button might be hidden if not w.isVisible():
continue yield w except Exception:
continue def show_menu_under_widget(gui, menu, action, name):
# First try the tool bar for w in toolbar_widgets_for_action(gui, action):
try:
# The w.height() assures that the menu opens below the button.
menu.exec(w.mapToGlobal(QPoint(0, w.height())))
return except Exception:
continue
# Now try the menu bar for x in gui.bars_manager.menu_bar.added_actions:
# This depends on no two menus with the same name.
# I don't know if this works on a Mac if x.text() == name:
try:
# The menu item might be hidden if not x.isVisible():
continue
# We can't use x.trigger() because it doesn't put the menu
# in the right place. Instead get the position of the menu
# widget on the menu bar p = x.parent().menu_bar r = p.actionGeometry(x)
# Make sure that the menu item is actually displayed in the menu
# and not the overflow if p.geometry().width() < (r.x() + r.width()):
continue
# Show the menu under the name in the menu bar menu.exec(p.mapToGlobal(QPoint(r.x()+2, r.height()-2)))
return except Exception:
continue
# Is it one of the status bar buttons?
for button in gui.status_bar_extra_buttons:
if name == button.action_name and button.isVisible():
r = button.geometry()
p = gui.status_bar menu.exec(p.mapToGlobal(QPoint(r.x()+2, r.height()-2)))
return
# No visible button found. Fall back to displaying in upper left corner
# of the library view.
menu.exec(gui.library_view.mapToGlobal(QPoint(10, 10)))

def menu_action_unique_name(plugin, unique_name):
return f'{plugin.unique_name} : menu action : {unique_name}'

[문서]
class InterfaceAction(QObject):
'''
A plugin representing an "action" that can be taken in the graphical user interface. All the items in the toolbar and context menus are implemented by these plugins.

Note that this class is the base class for these plugins, however, to integrate the plugin with calibre's plugin system, you have to make a wrapper class that references the actual plugin. See the
:mod:`calibre.customize.builtins` module for examples.

If two :class:`InterfaceAction` objects have the same name, the one with higher priority takes precedence.

Sub-classes should implement the :meth:`genesis`, :meth:`library_changed`,
:meth:`location_selected`, :meth:`shutting_down`,
:meth:`initialization_complete` and :meth:`tag_browser_context_action` methods.

Once initialized, this plugin has access to the main calibre GUI via the
:attr:`gui` member. You can access other plugins by name, for example::

self.gui.iactions['Save To Disk']

To access the actual plugin, use the :attr:`interface_action_base_plugin` attribute, this attribute only becomes available after the plugin has been initialized. Useful if you want to use methods from the plugin class like do_user_config().

The QAction specified by :attr:`action_spec` is automatically create and made available as ``self.qaction``.

'''

#: The plugin name. If two plugins with the same name are present, the one
#: with higher priority takes precedence.
name = 'Implement me'

#: The plugin priority. If two plugins with the same name are present, the one
#: with higher priority takes precedence.
priority = 1

#: The menu popup type for when this plugin is added to a toolbar popup_type = QToolButton.ToolButtonPopupMode.MenuButtonPopup

#: Whether this action should be auto repeated when its shortcut
#: key is held down.
auto_repeat = False

#: Of the form: (text, icon_path, tooltip, keyboard shortcut).
#: icon, tooltip and keyboard shortcut can be None.
#: keyboard shortcut must be either a string, None or tuple of shortcuts.
#: If None, a keyboard shortcut corresponding to the action is not
#: registered. If you pass an empty tuple, then the shortcut is registered
#: with no default key binding.
action_spec = ('text', 'icon', None, None)

#: If not None, used for the name displayed to the user when customizing
#: the keyboard shortcuts for the above action spec instead of action_spec[0]
action_shortcut_name = None

#: If True, a menu is automatically created and added to self.qaction action_add_menu = False

#: If True, a clone of self.qaction is added to the menu of self.qaction
#: If you want the text of this action to be different from that of
#: self.qaction, set this variable to the new text action_menu_clone_qaction = False

#: Set of locations to which this action must not be added.
#: See :attr:`all_locations` for a list of possible locations dont_add_to = frozenset()

#: Set of locations from which this action must not be removed.
#: See :attr:`all_locations` for a list of possible locations dont_remove_from = frozenset()

all_locations = frozenset(['toolbar', 'toolbar-device', 'context-menu',
'context-menu-device', 'toolbar-child', 'menubar', 'menubar-device',
'context-menu-cover-browser', 'context-menu-split', 'searchbar'])

#: Type of action
#: 'current' means acts on the current view
#: 'global' means an action that does not act on the current view, but rather
#: on calibre as a whole action_type = 'global'

#: If True, then this InterfaceAction will have the opportunity to interact
#: with drag and drop events. See the methods, :meth:`accept_enter_event`,
#: :meth`:accept_drag_move_event`, :meth:`drop_event` for details.
accepts_drops = False def __init__(self, parent, site_customization):
QObject.__init__(self, parent)
self.setObjectName(self.name)
self.gui = parent self.site_customization = site_customization self.interface_action_base_plugin = None

[문서]
def accept_enter_event(self, event, mime_data):
''' This method should return True iff this interface action is capable of handling the drag event. Do not call accept/ignore on the event, that will be taken care of by the calibre UI.'''
return False

[문서]
def accept_drag_move_event(self, event, mime_data):
''' This method should return True iff this interface action is capable of handling the drag event. Do not call accept/ignore on the event, that will be taken care of by the calibre UI.'''
return False

[문서]
def drop_event(self, event, mime_data):
''' This method should perform some useful action and return True iff this interface action is capable of handling the drop event. Do not call accept/ignore on the event, that will be taken care of by the calibre UI. You should not perform blocking/long operations in this function. Instead emit a signal or use QTimer.singleShot and return quickly. See the builtin actions for examples.'''
return False
def do_genesis(self):
self.Dispatcher = partial(Dispatcher, parent=self)
self.create_action()
self.gui.addAction(self.qaction)
self.gui.addAction(self.menuless_qaction)
self.genesis()
self.location_selected('library')

@property def unique_name(self):
bn = self.__class__.__name__ if getattr(self.interface_action_base_plugin, 'name'):
bn = self.interface_action_base_plugin.name return f'Interface Action: {bn} ({self.name})'

def create_action(self, spec=None, attr='qaction', shortcut_name=None, persist_shortcut=False):
if spec is None:
spec = self.action_spec text, icon, tooltip, shortcut = spec if icon is not None:
action = QAction(QIcon.ic(icon), text, self.gui)
else:
action = QAction(text, self.gui)
if attr == 'qaction':
if hasattr(self.action_menu_clone_qaction, 'rstrip'):
mt = str(self.action_menu_clone_qaction)
else:
mt = action.text()
self.menuless_qaction = ma = QAction(action.icon(), mt, self.gui)
ma.triggered.connect(action.trigger)
for a in ((action, ma) if attr == 'qaction' else (action,)):
a.setAutoRepeat(self.auto_repeat)
text = tooltip or text a.setToolTip(text)
a.setStatusTip(text)
a.setWhatsThis(text)
shortcut_action = action desc = tooltip or None if attr == 'qaction':
shortcut_action = ma if shortcut is not None:
keys = ((shortcut,) if isinstance(shortcut, (str, bytes)) else tuple(shortcut))
if shortcut_name is None:
if self.action_shortcut_name is not None:
shortcut_name = self.action_shortcut_name elif spec[0]:
shortcut_name = str(spec[0])
if shortcut_name and self.action_spec[0] and not ( attr == 'qaction' and self.popup_type == QToolButton.ToolButtonPopupMode.InstantPopup):
try:
self.gui.keyboard.register_shortcut(self.unique_name + ' - ' + attr, shortcut_name, default_keys=keys, action=shortcut_action, description=desc, group=self.action_spec[0], persist_shortcut=persist_shortcut)
except NameConflict as e:
try:
prints(str(e))
except Exception:
pass shortcut_action.setShortcuts([QKeySequence(key,
QKeySequence.SequenceFormat.PortableText) for key in keys])
else:
self.shortcut_action_for_context_menu = shortcut_action if ismacos:
# In Qt 5 keyboard shortcuts don't work unless the
# action is explicitly added to the main window self.gui.addAction(shortcut_action)

if attr is not None:
setattr(self, attr, action)
if attr == 'qaction' and self.action_add_menu:
menu = QMenu()
action.setMenu(menu)
if self.action_menu_clone_qaction:
menu.addAction(self.menuless_qaction)
return action

[문서]
def create_menu_action(self, menu, unique_name, text, icon=None, shortcut=None, description=None, triggered=None, shortcut_name=None, persist_shortcut=False):
'''
Convenience method to easily add actions to a QMenu.
Returns the created QAction. This action has one extra attribute calibre_shortcut_unique_name which if not None refers to the unique name under which this action is registered with the keyboard manager.

:param menu: The QMenu the newly created action will be added to
:param unique_name: A unique name for this action, this must be globally unique, so make it as descriptive as possible. If in doubt, add an UUID to it.
:param text: The text of the action.
:param icon: Either a QIcon or a file name. The file name is passed to the QIcon.ic() builtin, so you do not need to pass the full path to the images folder.
:param shortcut: A string, a list of strings, None or False. If False, no keyboard shortcut is registered for this action. If None, a keyboard shortcut with no default keybinding is registered. String and list of strings register a shortcut with default keybinding as specified.
:param description: A description for this action. Used to set tooltips.
:param triggered: A callable which is connected to the triggered signal of the created action.
:param shortcut_name: The text displayed to the user when customizing the keyboard shortcuts for this action. By default it is set to the value of ``text``.
:param persist_shortcut: Shortcuts for actions that don't always appear, or are library dependent, may disappear when other keyboard shortcuts are edited unless
```persist_shortcut``` is set True.

'''
if shortcut_name is None:
shortcut_name = str(text)
ac = menu.addAction(text)
if icon is not None:
if not isinstance(icon, QIcon):
icon = QIcon.ic(icon)
ac.setIcon(icon)
keys = ()
if shortcut is not None and shortcut is not False:
keys = ((shortcut,) if isinstance(shortcut, (str, bytes)) else tuple(shortcut))
unique_name = menu_action_unique_name(self, unique_name)
if description is not None:
ac.setToolTip(description)
ac.setStatusTip(description)
ac.setWhatsThis(description)

ac.calibre_shortcut_unique_name = unique_name if shortcut is not False:
self.gui.keyboard.register_shortcut(unique_name, shortcut_name, default_keys=keys, action=ac, description=description, group=self.action_spec[0], persist_shortcut=persist_shortcut)
# In Qt 5 keyboard shortcuts don't work unless the
# action is explicitly added to the main window and on OSX and
# Unity since the menu might be exported, the shortcuts won't work self.gui.addAction(ac)
if triggered is not None:
ac.triggered.connect(triggered)
return ac

[문서]
def load_resources(self, names):
'''
If this plugin comes in a ZIP file (user added plugin), this method will allow you to load resources from the ZIP file.

For example to load an image::

pixmap = QPixmap()
pixmap.loadFromData(tuple(self.load_resources(['images/icon.png']).values())[0])
icon = QIcon(pixmap)

:param names: List of paths to resources in the ZIP file using / as separator

:return: A dictionary of the form ``{name : file_contents}``. Any names that were not found in the ZIP file will not be present in the dictionary.

'''
if self.plugin_path is None:
raise ValueError('This plugin was not loaded from a ZIP file')
ans = {}
with ZipFile(self.plugin_path, 'r') as zf:
for candidate in zf.namelist():
if candidate in names:
ans[candidate] = zf.read(candidate)
return ans

[문서]
def genesis(self):
'''
Setup this plugin. Only called once during initialization. self.gui is available. The action specified by :attr:`action_spec` is available as
``self.qaction``.
'''
pass

[문서]
def location_selected(self, loc):
'''
Called whenever the book list being displayed in calibre changes.
Currently values for loc are: ``library, main, card and cardb``.

This method should enable/disable this action and its sub actions as appropriate for the location.
'''
pass

[문서]
def library_about_to_change(self, olddb, db):
'''
Called whenever the current library is changed.

:param olddb: The LibraryDatabase corresponding to the previous library.
:param db: The LibraryDatabase corresponding to the new library.

'''
pass

[문서]
def library_changed(self, db):
'''
Called whenever the current library is changed.

:param db: The LibraryDatabase corresponding to the current library.

'''
pass

[문서]
def gui_layout_complete(self):
'''
Called once per action when the layout of the main GUI is completed. If your action needs to make changes to the layout, they should be done here, rather than in :meth:`initialization_complete`.
'''
pass

[문서]
def initialization_complete(self):
'''
Called once per action when the initialization of the main GUI is completed.
'''
pass

[문서]
def tag_browser_context_action(self, index):
'''
Called when displaying the context menu in the Tag browser. ``index`` is the QModelIndex that points to the Tag browser item that was right clicked.
Test it for validity with index.valid() and get the underlying TagTreeItem object with index.data(Qt.ItemDataRole.UserRole). Any action objects yielded by this method will be added to the context menu.
'''
if False:
yield QAction()

[문서]
def shutting_down(self):
'''
Called once per plugin when the main GUI is in the process of shutting down. Release any used resources, but try not to block the shutdown for long periods of time.
'''
pass
class InterfaceActionWithLibraryDrop(InterfaceAction):
'''
Subclass of InterfaceAction that implements methods to execute the default action by drop some books from the library.

Inside the do_drop() method, the ids of the dropped books are provided by the attribute self.dropped_ids
'''

accepts_drops = True mimetype_for_drop = 'application/calibre+from_library'

def accept_enter_event(self, event, mime_data):
if mime_data.hasFormat(self.mimetype_for_drop):
return True return False def accept_drag_move_event(self, event, mime_data):
if mime_data.hasFormat(self.mimetype_for_drop):
return True return False def drop_event(self, event, mime_data):
if mime_data.hasFormat(self.mimetype_for_drop):
self.dropped_ids = tuple(map(int, mime_data.data(self.mimetype_for_drop).data().split()))
QTimer.singleShot(1, self.do_drop)
return True return False def do_drop(self):
raise NotImplementedError()

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 시작 »
- 모듈 코드 »
- calibre.gui2.preferences

calibre.gui2.preferences의 소스 코드

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import textwrap from functools import partial from qt.core import (
QAbstractSpinBox,
QApplication,
QCheckBox,
QComboBox,
QDialog,
QDialogButtonBox,
QEvent,
QGroupBox,
QIcon,
QLineEdit,
QListView,
QListWidget,
QRadioButton,
Qt,
QTableWidget,
QVBoxLayout,
QWidget, pyqtSignal,
)

from calibre.customize.ui import preferences_plugins from calibre.gui2.complete2 import EditWithComplete from calibre.gui2.widgets import HistoryLineEdit from calibre.utils.config import ConfigProxy class AbortCommit(Exception):
pass class AbortInitialize(Exception):
pass

[문서]
class ConfigWidgetInterface:
'''
This class defines the interface that all widgets displayed in the
Preferences dialog must implement. See :class:`ConfigWidgetBase` for a base class that implements this interface and defines various convenience methods as well.
'''

#: This signal must be emitted whenever the user changes a value in this
#: widget changed_signal = None

#: Set to True iff the :meth:`restore_to_defaults` method is implemented.
supports_restoring_to_defaults = True

#: The tooltip for the "Restore defaults" button restore_defaults_desc = _('Restore settings to default values. '
'You have to click Apply to actually save the default settings.')

#: If True the Preferences dialog will not allow the user to set any more
#: preferences. Only has effect if :meth:`commit` returns True.
restart_critical = False

[문서]
def genesis(self, gui):
'''
Called once before the widget is displayed, should perform any necessary setup.

:param gui: The main calibre graphical user interface
'''
raise NotImplementedError()

[문서]
def initialize(self):
'''
Should set all config values to their initial values (the values stored in the config files). A "return" statement is optional. Return
False if the dialog is not to be shown.
'''
raise NotImplementedError()

[문서]
def restore_defaults(self):
'''
Should set all config values to their defaults.
'''
pass

[문서]
def commit(self):
'''
Save any changed settings. Return True if the changes require a restart, False otherwise. Raise an :class:`AbortCommit` exception to indicate that an error occurred. You are responsible for giving the user feedback about what the error is and how to correct it.
'''
return False

[문서]
def refresh_gui(self, gui):
'''
Called once after this widget is committed. Responsible for causing the gui to reread any changed settings. Note that by default the GUI re-initializes various elements anyway, so most widgets won't need to use this method.
'''
pass

[문서]
def initial_tab_changed(self):
'''
Called if the initially displayed tab is changed before the widget is shown, but after it is initialized.
'''
pass
def do_on_child_tabs(self, method, *args):
r = False for t in self.child_tabs:
lazy_init_called = getattr(t, 'lazy_init_called', True)
if method in ('commit', 'refresh_gui') and not lazy_init_called:
continue if method == 'restore_defaults' and not lazy_init_called:
if hasattr(t, 'lazy_initialize'):
t.lazy_initialize()
t.lazy_init_called = True r = r | bool(getattr(t, method)(*args))
r = r | bool(t.do_on_child_tabs(method, *args))
return rdef set_help_tips(gui_obj, tt):
if tt:
if not str(gui_obj.whatsThis()):
gui_obj.setWhatsThis(tt)
if not str(gui_obj.statusTip()):
gui_obj.setStatusTip(tt)
tt = '\n'.join(textwrap.wrap(tt, 70))
gui_obj.setToolTip(tt)

class Setting:

CHOICES_SEARCH_FLAGS = Qt.MatchFlag.MatchExactly | Qt.MatchFlag.MatchCaseSensitive def __init__(self, name, config_obj, widget, gui_name=None, empty_string_is_None=True, choices=None, restart_required=False):
self.name, self.gui_name = name, gui_name self.empty_string_is_None = empty_string_is_None self.restart_required = restart_required self.choices = choices if gui_name is None:
self.gui_name = 'opt_'+name self.config_obj = config_obj self.gui_obj = getattr(widget, self.gui_name)
self.widget = widget if isinstance(self.gui_obj, QCheckBox):
self.datatype = 'bool'
self.gui_obj.stateChanged.connect(self.changed)
elif isinstance(self.gui_obj, QGroupBox) and self.gui_obj.isCheckable():
self.datatype = 'bool'
self.gui_obj.clicked.connect(self.changed)
elif isinstance(self.gui_obj, QRadioButton):
self.datatype = 'bool'
self.gui_obj.toggled.connect(self.changed)
elif isinstance(self.gui_obj, QAbstractSpinBox):
self.datatype = 'number'
self.gui_obj.valueChanged.connect(self.changed)
elif isinstance(self.gui_obj, (QLineEdit, HistoryLineEdit)):
self.datatype = 'string'
self.gui_obj.textChanged.connect(self.changed)
if isinstance(self.gui_obj, HistoryLineEdit):
self.gui_obj.initialize('preferences_setting_' + self.name)
elif isinstance(self.gui_obj, QComboBox):
self.datatype = 'choice'
self.gui_obj.editTextChanged.connect(self.changed)
self.gui_obj.currentIndexChanged.connect(self.changed)
else:
raise ValueError(f'Unknown data type {self.gui_obj.__class__}')

if isinstance(self.config_obj, ConfigProxy) and \ not str(self.gui_obj.toolTip()):
h = self.config_obj.help(self.name)
if h:
self.gui_obj.setToolTip(h)
tt = str(self.gui_obj.toolTip())
set_help_tips(self.gui_obj, tt)

def changed(self, *args):
self.widget.changed_signal.emit()

def initialize(self):
self.gui_obj.blockSignals(True)
if self.datatype == 'choice':
choices = self.choices or []
if isinstance(self.gui_obj, EditWithComplete):
self.gui_obj.all_items = choices else:
self.gui_obj.clear()
for x in choices:
if isinstance(x, (str, bytes)):
x = (x, x)
self.gui_obj.addItem(x[0], (x[1]))
self.set_gui_val(self.get_config_val(default=False))
self.gui_obj.blockSignals(False)
self.initial_value = self.get_gui_val()

def commit(self):
val = self.get_gui_val()
oldval = self.get_config_val()
changed = val != oldval if changed:
self.set_config_val(self.get_gui_val())
return changed and self.restart_required def restore_defaults(self):
self.set_gui_val(self.get_config_val(default=True))

def get_config_val(self, default=False):
if default:
val = self.config_obj.defaults[self.name]
else:
val = self.config_obj[self.name]
return val def set_config_val(self, val):
self.config_obj[self.name] = val def set_gui_val(self, val):
if self.datatype == 'bool':
self.gui_obj.setChecked(bool(val))
elif self.datatype == 'number':
self.gui_obj.setValue(val)
elif self.datatype == 'string':
self.gui_obj.setText(val or '')
elif self.datatype == 'choice':
if isinstance(self.gui_obj, EditWithComplete):
self.gui_obj.setText(val)
else:
idx = self.gui_obj.findData((val), role=Qt.ItemDataRole.UserRole, flags=self.CHOICES_SEARCH_FLAGS)
if idx == -1:
idx = 0 self.gui_obj.setCurrentIndex(idx)

def get_gui_val(self):
if self.datatype == 'bool':
val = bool(self.gui_obj.isChecked())
elif self.datatype == 'number':
val = self.gui_obj.value()
elif self.datatype == 'string':
val = str(self.gui_obj.text()).strip()
if self.empty_string_is_None and not val:
val = None elif self.datatype == 'choice':
if isinstance(self.gui_obj, EditWithComplete):
val = str(self.gui_obj.text())
else:
idx = self.gui_obj.currentIndex()
idx = max(idx, 0)
val = str(self.gui_obj.itemData(idx) or '')
return val class CommaSeparatedList(Setting):

def set_gui_val(self, val):
x = ''
if val:
x = ', '.join(val)
self.gui_obj.setText(x)

def get_gui_val(self):
val = str(self.gui_obj.text()).strip()
ans = []
if val:
ans = [x.strip() for x in val.split(',')]
ans = [x for x in ans if x]
return ans

[문서]
class ConfigWidgetBase(QWidget, ConfigWidgetInterface):
'''
Base class that contains code to easily add standard config widgets like checkboxes, combo boxes, text fields and so on. See the :meth:`register` method.

This class automatically handles change notification, resetting to default, translation between gui objects and config objects, etc. for registered settings.

If your config widget inherits from this class but includes setting that are not registered, you should override the :class:`ConfigWidgetInterface` methods and call the base class methods inside the overrides.
'''

changed_signal = pyqtSignal()
restart_now = pyqtSignal()
supports_restoring_to_defaults = True restart_critical = False def __init__(self, parent=None):
QWidget.__init__(self, parent)
if hasattr(self, 'setupUi'):
self.setupUi(self)
self.settings = {}
self.child_tabs = []
for v in self.__dict__.values():
if isinstance(v, LazyConfigWidgetBase):
self.child_tabs.append(v)

[문서]
def register(self, name, config_obj, gui_name=None, choices=None, restart_required=False, empty_string_is_None=True, setting=Setting):
'''
Register a setting.

:param name: The setting name
:param config_obj: The config object that reads/writes the setting
:param gui_name: The name of the GUI object that presents an interface to change the setting. By default it is assumed to be
``'opt_' + name``.
:param choices: If this setting is a multiple choice (combobox) based setting, the list of choices. The list is a list of two element tuples of the form: ``[(gui name, value), ...]``
:param setting: The class responsible for managing this setting. The default class handles almost all cases, so this param is rarely used.
'''
setting = setting(name, config_obj, self, gui_name=gui_name, choices=choices, restart_required=restart_required, empty_string_is_None=empty_string_is_None)
return self.register_setting(setting)

def register_setting(self, setting):
self.settings[setting.name] = setting return setting

[문서]
def initialize(self):
for setting in self.settings.values():
setting.initialize()

[문서]
def commit(self, *args):
restart_required = False for setting in self.settings.values():
rr = setting.commit()
if rr:
restart_required = True return restart_required

[문서]
def restore_defaults(self, *args):
for setting in self.settings.values():
setting.restore_defaults()

def register_child_tab(self, tab):
self.child_tabs.append(tab)def get_plugin(category, name):
for plugin in preferences_plugins():
if plugin.category == category and plugin.name == name:
return plugin raise ValueError( f'No Preferences Plugin with category: {category} and name: {name} found')

class LazyConfigWidgetBase(ConfigWidgetBase):
'''
Use this for dialogs that are tabs, accessed either from the left or on the top. It directly replaces ConfigWidgetBase, supporting the lazy operations.
'''

def __init__(self, parent=None):
super().__init__(parent)
self.lazy_init_called = False def ensure_lazy_initialized(self):
if not self.lazy_init_called:
if hasattr(self, 'lazy_initialize'):
self.lazy_initialize()
self.lazy_init_called = True def set_changed_signal(self, changed_signal):
self.changed_signal.connect(changed_signal)

def restore_defaults(self):
self.ensure_lazy_initialized()
super().restore_defaults()

def showEvent(self, event):
# called when the widget is actually displays. We can't do something like
# lazy_genesis because Qt does "things" before showEvent() is called. In
# particular, the register function doesn't work with combo boxes if
# genesis isn't called before everything else. Why is a mystery.
self.ensure_lazy_initialized()
super().showEvent(event)

class ConfigDialog(QDialog):

def set_widget(self, w):
self.w = w def accept(self):
try:
self.restart_required = self.w.commit()
except AbortCommit:
return
QDialog.accept(self)

def init_gui():
from calibre.gui2.main import option_parser from calibre.gui2.ui import Main from calibre.library import db parser = option_parser()
opts, args = parser.parse_args([])
actions = tuple(Main.create_application_menubar())
db = db()
gui = Main(opts)
gui.initialize(db.library_path, db, actions, show_gui=False)
return gui def show_config_widget(category, name, gui=None, show_restart_msg=False, parent=None, never_shutdown=False, callback=None):
'''
Show the preferences plugin identified by category and name

:param gui: gui instance, if None a hidden gui is created
:param show_restart_msg: If True and the preferences plugin indicates a restart is required, show a message box telling the user to restart
:param parent: The parent of the displayed dialog

:return: True iff a restart is required for the changes made by the user to take effect
'''
from calibre.gui2 import gprefs pl = get_plugin(category, name)
d = ConfigDialog(parent)
d.resize(750, 550)
conf_name = f'config_widget_dialog_geometry_{category}_{name}'
d.setWindowTitle(_('Configure ') + pl.gui_name)
d.setWindowIcon(QIcon.ic('config.png'))
bb = QDialogButtonBox(d)
bb.setStandardButtons(QDialogButtonBox.StandardButton.Apply|QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.RestoreDefaults)
bb.accepted.connect(d.accept)
bb.rejected.connect(d.reject)
w = pl.create_widget(d)
d.set_widget(w)
bb.button(QDialogButtonBox.StandardButton.RestoreDefaults).clicked.connect(w.restore_defaults)
bb.button(QDialogButtonBox.StandardButton.RestoreDefaults).setEnabled(w.supports_restoring_to_defaults)
bb.button(QDialogButtonBox.StandardButton.Apply).setEnabled(False)
bb.button(QDialogButtonBox.StandardButton.Apply).clicked.connect(d.accept)

def onchange():
b = bb.button(QDialogButtonBox.StandardButton.Apply)
b.setEnabled(True)
b.setDefault(True)
b.setAutoDefault(True)
w.changed_signal.connect(onchange)
bb.button(QDialogButtonBox.StandardButton.Cancel).setFocus(Qt.FocusReason.OtherFocusReason)
l = QVBoxLayout()
d.setLayout(l)
l.addWidget(w)
l.addWidget(bb)
mygui = gui is None if gui is None:
gui = init_gui()
mygui = True w.genesis(gui)
w.do_on_child_tabs('genesis', gui)
w.initialize()
w.do_on_child_tabs('initialize')
d.restore_geometry(gprefs, conf_name)
if callback is not None:
callback(w)
d.exec()
d.save_geometry(gprefs, conf_name)
rr = getattr(d, 'restart_required', False)
if show_restart_msg and rr:
from calibre.gui2 import warning_dialog warning_dialog(gui, 'Restart required', 'Restart required', show=True)
if mygui and not never_shutdown:
gui.shutdown()
return rr class ListViewWithMoveByKeyPress(QListView):

def set_movement_functions(self, up_function, down_function):
self.up_function = partial(up_function, use_kbd_modifiers=False)
self.down_function = partial(down_function, use_kbd_modifiers=False)

def event(self, event):
if (event.type() == QEvent.KeyPress and
QApplication.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier):
if event.key() == Qt.Key.Key_Up:
self.up_function()
elif event.key() == Qt.Key.Key_Down:
self.down_function()
return True return QListView.event(self, event)

class ListWidgetWithMoveByKeyPress(QListWidget):

def set_movement_functions(self, up_function, down_function):
self.up_function = partial(up_function, use_kbd_modifiers=False)
self.down_function = partial(down_function, use_kbd_modifiers=False)

def event(self, event):
if (event.type() == QEvent.KeyPress and
QApplication.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier):
if event.key() == Qt.Key.Key_Up:
self.up_function()
elif event.key() == Qt.Key.Key_Down:
self.down_function()
return True return QListWidget.event(self, event)

class TableWidgetWithMoveByKeyPress(QTableWidget):

def set_movement_functions(self, up_function, down_function):
self.up_function = partial(up_function, use_kbd_modifiers=False)
self.down_function = partial(down_function, use_kbd_modifiers=False)

def event(self, event):
if (event.type() == QEvent.KeyPress and
QApplication.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier):
if event.key() == Qt.Key.Key_Up:
self.up_function()
elif event.key() == Qt.Key.Key_Down:
self.down_function()
return True return QTableWidget.event(self, event)

def get_move_count(row_count):
mods = QApplication.keyboardModifiers() & (
Qt.KeyboardModifier.ShiftModifier | Qt.KeyboardModifier.ControlModifier |Qt.KeyboardModifier.AltModifier | Qt.KeyboardModifier.MetaModifier)
return {Qt.KeyboardModifier.ShiftModifier | Qt.KeyboardModifier.ControlModifier: row_count,
Qt.KeyboardModifier.ShiftModifier: 5, Qt.KeyboardModifier.ControlModifier: 10}.get(mods, 1)

# Testing {{{ def test_widget(category, name, gui=None, callback=None):
show_config_widget(category, name, gui=gui, show_restart_msg=True, callback=callback)

def test_all():
from qt.core import QApplication app = QApplication([])
app gui = init_gui()
for plugin in preferences_plugins():
test_widget(plugin.category, plugin.name, gui=gui)
gui.shutdown()

if __name__ == '__main__':
test_all()
# }}}

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 시작 »
- 모듈 코드 »
- calibre.gui2.tweak_book.boss

calibre.gui2.tweak_book.boss의 소스 코드

#!/usr/bin/env python
# License: GPLv3 Copyright: 2013, Kovid Goyal <kovid at kovidgoyal.net> import errno import os import shutil import subprocess import sys import tempfile from functools import partial, wraps from urllib.parse import urlparse from qt.core import (
QApplication,
QCheckBox,
QDialog,
QDialogButtonBox,
QGridLayout,
QIcon,
QInputDialog,
QLabel,
QMimeData,
QObject,
QSize,
Qt,
QTimer,
QUrl,
QVBoxLayout, pyqtSignal,
)

from calibre import isbytestring, prints from calibre.constants import cache_dir, islinux, ismacos, iswindows from calibre.ebooks.oeb.base import urlnormalize from calibre.ebooks.oeb.polish.container import OEB_DOCS, OEB_STYLES, clone_container, guess_type from calibre.ebooks.oeb.polish.container import get_container as _gc from calibre.ebooks.oeb.polish.cover import mark_as_cover, mark_as_titlepage, set_cover from calibre.ebooks.oeb.polish.css import filter_css, rename_class from calibre.ebooks.oeb.polish.main import SUPPORTED, tweak_polish from calibre.ebooks.oeb.polish.pretty import fix_all_html, pretty_all from calibre.ebooks.oeb.polish.replace import get_recommended_folders, rationalize_folders, rename_files, replace_file from calibre.ebooks.oeb.polish.split import AbortError, merge, multisplit, split from calibre.ebooks.oeb.polish.toc import create_inline_toc, mark_as_nav, remove_names_from_toc from calibre.ebooks.oeb.polish.utils import link_stylesheets from calibre.ebooks.oeb.polish.utils import setup_css_parser_serialization as scs from calibre.gui2 import ( add_to_recent_docs, choose_dir, choose_files, choose_save_file, error_dialog, info_dialog, open_url, question_dialog, sanitize_env_vars, warning_dialog,
)
from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.tweak_book import actions, current_container, dictionaries, editor_name, editors, set_book_locale, set_current_container, tprefs from calibre.gui2.tweak_book.completion.worker import completion_worker from calibre.gui2.tweak_book.editor import editor_from_syntax, syntax_from_mime from calibre.gui2.tweak_book.editor.insert_resource import NewBook, get_resource_data from calibre.gui2.tweak_book.file_list import FILE_COPY_MIME, NewFileDialog from calibre.gui2.tweak_book.preferences import Preferences from calibre.gui2.tweak_book.preview import parse_worker from calibre.gui2.tweak_book.save import SaveManager, find_first_existing_ancestor, save_container from calibre.gui2.tweak_book.search import run_search, validate_search_request from calibre.gui2.tweak_book.spell import find_next as find_next_word from calibre.gui2.tweak_book.spell import find_next_error from calibre.gui2.tweak_book.toc import TOCEditor from calibre.gui2.tweak_book.undo import GlobalUndoHistory from calibre.gui2.tweak_book.widgets import (
AddCover,
FilterCSS,
ImportForeign,
InsertLink,
InsertSemantics,
InsertTag,
MultiSplit,
QuickOpen,
RationalizeFolders,
)
from calibre.gui2.widgets import BusyCursor from calibre.ptempfile import PersistentTemporaryDirectory, TemporaryDirectory from calibre.startup import connect_lambda from calibre.utils.config import JSONConfig from calibre.utils.icu import numeric_sort_key from calibre.utils.imghdr import identify from calibre.utils.ipc.launch import exe_path, macos_edit_book_bundle_path from calibre.utils.localization import ngettext from calibre.utils.tdir_in_cache import tdir_in_cache from polyglot.builtins import as_bytes

_diff_dialogs = []
last_used_transform_rules = []
last_used_html_transform_rules = []

def get_container(*args, **kwargs):
kwargs['tweak_mode'] = True container = _gc(*args, **kwargs)
return container def setup_css_parser_serialization():
scs(tprefs['editor_tab_stop_width'])

def in_thread_job(func):
@wraps(func)
def ans(*args, **kwargs):
with BusyCursor():
return func(*args, **kwargs)
return ans def get_boss():
return get_boss.boss def open_path_in_new_editor_instance(path: str):
import subprocess from calibre.gui2 import sanitize_env_vars with sanitize_env_vars():
if ismacos:
from calibre.utils.ipc.launch import macos_edit_book_bundle_path bundle = os.path.dirname(os.path.dirname(macos_edit_book_bundle_path().rstrip('/')))
subprocess.Popen(['open', '-n', '-a', bundle, path])
else:
subprocess.Popen([sys.executable, path])

[문서]
class Boss(QObject):

handle_completion_result_signal = pyqtSignal(object)

def __init__(self, parent, notify=None):
QObject.__init__(self, parent)
self.global_undo = GlobalUndoHistory()
self.file_was_readonly = False self.container_count = 0 self.tdir = None self.save_manager = SaveManager(parent, notify)
self.save_manager.report_error.connect(self.report_save_error)
self.save_manager.check_for_completion.connect(self.check_terminal_save)
self.doing_terminal_save = False self.ignore_preview_to_editor_sync = False setup_css_parser_serialization()
get_boss.boss = self self.gui = parent completion_worker().result_callback = self.handle_completion_result_signal.emit self.handle_completion_result_signal.connect(self.handle_completion_result, Qt.ConnectionType.QueuedConnection)
self.completion_request_count = 0 self.editor_cache = JSONConfig('editor-cache', base_path=cache_dir())
d = self.editor_cache.defaults d['edit_book_state'] = {}
d['edit_book_state_order'] = []

def __call__(self, gui):
self.gui = gui gui.message_popup.undo_requested.connect(self.do_global_undo)
fl = gui.file_list fl.delete_requested.connect(self.delete_requested)
fl.reorder_spine.connect(self.reorder_spine)
fl.rename_requested.connect(self.rename_requested)
fl.bulk_rename_requested.connect(self.bulk_rename_requested)
fl.edit_file.connect(self.edit_file_requested)
fl.merge_requested.connect(self.merge_requested)
fl.mark_requested.connect(self.mark_requested)
fl.export_requested.connect(self.export_requested)
fl.replace_requested.connect(self.replace_requested)
fl.link_stylesheets_requested.connect(self.link_stylesheets_requested)
fl.initiate_file_copy.connect(self.copy_files_to_clipboard)
fl.initiate_file_paste.connect(self.paste_files_from_clipboard)
fl.open_file_with.connect(self.open_file_with)
self.gui.central.current_editor_changed.connect(self.apply_current_editor_state)
self.gui.central.close_requested.connect(self.editor_close_requested)
self.gui.central.search_panel.search_triggered.connect(self.search)
self.gui.text_search.find_text.connect(self.find_text)
self.gui.preview.sync_requested.connect(self.sync_editor_to_preview)
self.gui.preview.split_start_requested.connect(self.split_start_requested)
self.gui.preview.split_requested.connect(self.split_requested)
self.gui.preview.link_clicked.connect(self.link_clicked)
self.gui.preview.open_file_with.connect(self.open_file_with)
self.gui.preview.edit_file.connect(self.edit_file_requested)
self.gui.check_book.item_activated.connect(self.check_item_activated)
self.gui.check_book.check_requested.connect(self.check_requested)
self.gui.check_book.fix_requested.connect(self.fix_requested)
self.gui.toc_view.navigate_requested.connect(self.link_clicked)
self.gui.toc_view.refresh_requested.connect(self.commit_all_editors_to_container)
self.gui.image_browser.image_activated.connect(self.image_activated)
self.gui.checkpoints.revert_requested.connect(self.revert_requested)
self.gui.checkpoints.compare_requested.connect(self.compare_requested)
self.gui.saved_searches.run_saved_searches.connect(self.run_saved_searches)
self.gui.saved_searches.copy_search_to_search_panel.connect(self.gui.central.search_panel.paste_saved_search)
self.gui.central.search_panel.save_search.connect(self.save_search)
self.gui.central.search_panel.show_saved_searches.connect(self.show_saved_searches)
self.gui.spell_check.find_word.connect(self.find_word)
self.gui.spell_check.refresh_requested.connect(self.commit_all_editors_to_container)
self.gui.spell_check.word_replaced.connect(self.word_replaced)
self.gui.spell_check.word_ignored.connect(self.word_ignored)
self.gui.spell_check.change_requested.connect(self.word_change_requested)
self.gui.live_css.goto_declaration.connect(self.goto_style_declaration)
self.gui.manage_fonts.container_changed.connect(self.apply_container_update_to_gui)
self.gui.manage_fonts.embed_all_fonts.connect(self.manage_fonts_embed)
self.gui.manage_fonts.subset_all_fonts.connect(self.manage_fonts_subset)
self.gui.reports.edit_requested.connect(self.reports_edit_requested)
self.gui.reports.refresh_starting.connect(self.commit_all_editors_to_container)
self.gui.reports.delete_requested.connect(self.delete_requested)

@property def currently_editing(self):
' Return the name of the file being edited currently or None if no file is being edited '
return editor_name(self.gui.central.current_editor)

def preferences(self):
orig_spell = tprefs['inline_spell_check']
orig_size = tprefs['toolbar_icon_size']
p = Preferences(self.gui)
ret = p.exec()
if p.dictionaries_changed:
dictionaries.clear_caches()
dictionaries.initialize(force=True) # Reread user dictionaries if p.toolbars_changed:
self.gui.populate_toolbars()
for ed in editors.values():
if hasattr(ed, 'populate_toolbars'):
ed.populate_toolbars()
if orig_size != tprefs['toolbar_icon_size']:
for ed in editors.values():
if hasattr(ed, 'bars'):
for bar in ed.bars:
bar.setIconSize(QSize(tprefs['toolbar_icon_size'], tprefs['toolbar_icon_size']))

if ret == QDialog.DialogCode.Accepted:
setup_css_parser_serialization()
self.gui.apply_settings()
self.refresh_file_list()
self.gui.preview.start_refresh_timer()
if ret == QDialog.DialogCode.Accepted or p.dictionaries_changed:
for ed in editors.values():
ed.apply_settings(dictionaries_changed=p.dictionaries_changed)
if orig_spell != tprefs['inline_spell_check']:
from calibre.gui2.tweak_book.editor.syntax.html import refresh_spell_check_status refresh_spell_check_status()
for ed in editors.values():
try:
ed.editor.highlighter.rehighlight()
except AttributeError:
pass def mark_requested(self, name, action):
self.commit_dirty_opf()
c = current_container()
if action == 'cover':
mark_as_cover(current_container(), name)
elif action.startswith('titlepage:'):
action, move_to_start = action.partition(':')[0::2]
move_to_start = move_to_start == 'True'
mark_as_titlepage(current_container(), name, move_to_start=move_to_start)
elif action == 'nav':
mark_as_nav(current_container(), name)

if c.opf_name in editors:
editors[c.opf_name].replace_data(c.raw_data(c.opf_name))
self.gui.file_list.build(c)
self.set_modified()

def mkdtemp(self, prefix=''):
self.container_count += 1 return tempfile.mkdtemp(prefix=f'{prefix}{self.container_count:05}-', dir=self.tdir)

def _check_before_open(self):
if self.gui.action_save.isEnabled():
if not question_dialog(self.gui, _('Unsaved changes'), _(
'The current book has unsaved changes. If you open a new book, they will be lost.'
' Are you sure you want to proceed?')):
return if self.save_manager.has_tasks:
return info_dialog(self.gui, _('Cannot open'),
_('The current book is being saved, you cannot open a new book until'
' the saving is completed'), show=True)
return True def new_book(self):
if not self._check_before_open():
return d = NewBook(self.gui)
if d.exec() == QDialog.DialogCode.Accepted:
fmt = d.fmt.lower()
path = choose_save_file(self.gui, 'edit-book-new-book', _('Choose file location'), filters=[(fmt.upper(), (fmt,))], all_files=False)
if path is not None:
if not path.lower().endswith('.' + fmt):
path = path + '.' + fmt from calibre.ebooks.oeb.polish.create import create_book create_book(d.mi, path, fmt=fmt)
self.open_book(path=path)

def import_book(self, path=None):
if not self._check_before_open():
return d = ImportForeign(self.gui)
if hasattr(path, 'rstrip'):
d.set_src(os.path.abspath(path))
if d.exec() == QDialog.DialogCode.Accepted:
for name in tuple(editors):
self.close_editor(name)
from calibre.ebooks.oeb.polish.import_book import import_book_as_epub src, dest = d.data self._clear_notify_data = True def func(src, dest, tdir):
import_book_as_epub(src, dest)
return get_container(dest, tdir=tdir)
self.gui.blocking_job('import_book', _('Importing book, please wait...'), self.book_opened, func, src, dest, tdir=self.mkdtemp())

[문서]
def open_book(self, path=None, edit_file=None, clear_notify_data=True, open_folder=False, search_text=None):
'''
Open the e-book at ``path`` for editing. Will show an error if the e-book is not in a supported format or the current book has unsaved changes.

:param edit_file: The name of a file inside the newly opened book to start editing. Can also be a list of names.
'''
if isinstance(path, (list, tuple)) and path:
# Can happen from an file_event_hook on OS X when drag and dropping
# onto the icon in the dock or using open -a extra_paths = path[1:]
path = path[0]
for x in extra_paths:
open_path_in_new_editor_instance(x)
if not self._check_before_open():
return if not hasattr(path, 'rpartition'):
if open_folder:
path = choose_dir(self.gui, 'open-book-folder-for-tweaking', _('Choose book folder'))
if path:
path = [path]
else:
path = choose_files(self.gui, 'open-book-for-tweaking', _('Choose book'),
[(_('Books'), [x.lower() for x in SUPPORTED])], all_files=False, select_only_single_file=True)

if not path:
return path = path[0]

if not os.path.exists(path):
return error_dialog(self.gui, _('File not found'), _(
'The file %s does not exist.') % path, show=True)
isdir = os.path.isdir(path)
ext = path.rpartition('.')[-1].upper()
if ext not in SUPPORTED and not isdir:
from calibre.ebooks.oeb.polish.import_book import IMPORTABLE if ext.lower() in IMPORTABLE:
return self.import_book(path)
return error_dialog(self.gui, _('Unsupported format'),
_('Tweaking is only supported for books in the %s formats.'
' Convert your book to one of these formats first.') % _(' and ').join(sorted(SUPPORTED)), show=True)

self.file_was_readonly = not os.access(path, os.W_OK)
if self.file_was_readonly:
warning_dialog(self.gui, _('Read-only file'), _(
'The file {} is read-only. Saving changes to it will either fail or cause its permissions to be reset.').format(path), show=True)
with self.editor_cache:
self.save_book_edit_state()

for name in tuple(editors):
self.close_editor(name)
self.gui.preview.clear()
self.gui.live_css.clear()
self.container_count = -1 if self.tdir:
shutil.rmtree(self.tdir, ignore_errors=True)
# We use the cache dir rather than the temporary dir to try and prevent
# temp file cleaners from nuking ebooks. See https://bugs.launchpad.net/bugs/1740460 self.tdir = tdir_in_cache('ee')
self._edit_file_on_open = edit_file self._search_text_on_open = search_text self._clear_notify_data = clear_notify_data self.gui.blocking_job('open_book', _('Opening book, please wait...'), self.book_opened, get_container, path, tdir=self.mkdtemp())def book_opened(self, job):
ef = getattr(self, '_edit_file_on_open', None)
cn = getattr(self, '_clear_notify_data', True)
st = getattr(self, '_search_text_on_open', None)
self._edit_file_on_open = self._search_text_on_open = None if job.traceback is not None:
self.gui.update_status_bar_default_message()
if 'DRMError:' in job.traceback:
from calibre.gui2.dialogs.drm_error import DRMErrorMessage return DRMErrorMessage(self.gui).exec()
if 'ObfuscationKeyMissing:' in job.traceback:
return error_dialog(self.gui, _('Failed to open book'), _(
'Failed to open book, it has obfuscated fonts, but the obfuscation key is missing from the OPF.'
' Do an EPUB to EPUB conversion before trying to edit this book.'), show=True)

return error_dialog(self.gui, _('Failed to open book'),
_('Failed to open book, click "Show details" for more information.'), det_msg=job.traceback, show=True)
if cn:
self.save_manager.clear_notify_data()
self.gui.check_book.clear_at_startup()
self.gui.spell_check.clear_caches()
dictionaries.clear_ignored(), dictionaries.clear_caches()
parse_worker.clear()
container = job.result set_current_container(container)
completion_worker().clear_caches()
with BusyCursor():
self.current_metadata = self.gui.current_metadata = container.mi lang = container.opf_xpath('//dc:language/text()') or [self.current_metadata.language]
set_book_locale(lang[0])
self.global_undo.open_book(container)
self.gui.update_window_title()
self.gui.file_list.current_edited_name = None self.gui.file_list.build(container, preserve_state=False)
self.gui.action_save.setEnabled(False)
self.update_global_history_actions()
recent_books = list(tprefs.get('recent-books', []))
path = os.path.abspath(container.path_to_ebook)
if path in recent_books:
recent_books.remove(path)
self.gui.update_status_bar_default_message(path)
recent_books.insert(0, path)
tprefs['recent-books'] = recent_books[:10]
self.gui.update_recent_books()
if iswindows:
try:
add_to_recent_docs(path)
except Exception:
import traceback traceback.print_exc()
if ef:
if isinstance(ef, str):
ef = [ef]
for i in ef:
self.gui.file_list.request_edit(i)
elif tprefs['restore_book_state']:
self.restore_book_edit_state()
self.gui.toc_view.update_if_visible()
self.add_savepoint(_('Start of editing session'))
if st:
self.find_initial_text(st)

def update_editors_from_container(self, container=None, names=None):
c = container or current_container()
for name, ed in tuple(editors.items()):
if c.has_name(name):
if names is None or name in names:
ed.replace_data(c.raw_data(name))
ed.is_synced_to_container = True else:
self.close_editor(name)

def refresh_file_list(self):
container = current_container()
self.gui.file_list.build(container)
completion_worker().clear_caches('names')

[문서]
def apply_container_update_to_gui(self, mark_as_modified=True):
'''
Update all the components of the user interface to reflect the latest data in the current book container.

:param mark_as_modified: If True, the book will be marked as modified, so the user will be prompted to save it when quitting.
'''
self.refresh_file_list()
self.update_global_history_actions()
self.update_editors_from_container()
if mark_as_modified:
self.set_modified()
self.gui.toc_view.update_if_visible()
completion_worker().clear_caches()
self.gui.preview.start_refresh_timer()

@in_thread_job def delete_requested(self, spine_items, other_items):
self.add_savepoint(_('Before: Delete files'))
self.commit_dirty_opf()
c = current_container()
c.remove_from_spine(spine_items)
for name in other_items:
c.remove_item(name)
self.set_modified()
self.gui.file_list.delete_done(spine_items, other_items)
spine_names = [x for x, remove in spine_items if remove]
completion_worker().clear_caches('names')
items = spine_names + list(other_items)
for name in items:
if name in editors:
self.close_editor(name)
if not editors:
self.gui.preview.clear()
self.gui.live_css.clear()
changed = remove_names_from_toc(current_container(), spine_names + list(other_items))
if changed:
self.gui.toc_view.update_if_visible()
for toc in changed:
if toc and toc in editors:
editors[toc].replace_data(c.raw_data(toc))
if c.opf_name in editors:
editors[c.opf_name].replace_data(c.raw_data(c.opf_name))
self.gui.message_popup(ngettext(
'One file deleted', '{} files deleted', len(items)).format(len(items)))

def commit_dirty_opf(self):
c = current_container()
if c.opf_name in editors and not editors[c.opf_name].is_synced_to_container:
self.commit_editor_to_container(c.opf_name)
self.gui.update_window_title()

def reorder_spine(self, items):
if not self.ensure_book():
return self.add_savepoint(_('Before: Re-order text'))
c = current_container()
c.set_spine(items)
self.set_modified()
self.gui.file_list.build(current_container()) # needed as the linear flag may have changed on some items if c.opf_name in editors:
editors[c.opf_name].replace_data(c.raw_data(c.opf_name))
completion_worker().clear_caches('names')

def add_file(self):
if not self.ensure_book(_('You must first open a book to edit, before trying to create new files in it.')):
return self.commit_dirty_opf()
d = NewFileDialog(self.gui)
if d.exec() != QDialog.DialogCode.Accepted:
return added_name = self.do_add_file(d.file_name, d.file_data, using_template=d.using_template, edit_file=True)
if d.file_name.rpartition('.')[2].lower() in ('ttf', 'otf', 'woff'):
from calibre.gui2.tweak_book.manage_fonts import show_font_face_rule_for_font_file show_font_face_rule_for_font_file(d.file_data, added_name, self.gui)

def do_add_file(self, file_name, data, using_template=False, edit_file=False):
self.add_savepoint(_('Before: Add file %s') % self.gui.elided_text(file_name))
c = current_container()
adata = data.replace(b'%CURSOR%', b'') if using_template else data spine_index = c.index_in_spine(self.currently_editing or '')
if spine_index is not None:
spine_index += 1 try:
added_name = c.add_file(file_name, adata, spine_index=spine_index)
except Exception:
self.rewind_savepoint()
raise self.gui.file_list.build(c)
self.gui.file_list.select_name(file_name)
if c.opf_name in editors:
editors[c.opf_name].replace_data(c.raw_data(c.opf_name))
mt = c.mime_map[file_name]
syntax = syntax_from_mime(file_name, mt)
if syntax and edit_file:
if using_template:
self.edit_file(file_name, syntax, use_template=data.decode('utf-8'))
else:
self.edit_file(file_name, syntax)
self.set_modified()
completion_worker().clear_caches('names')
return added_name def add_files(self):
if not self.ensure_book(_('You must first open a book to edit, before trying to create new files in it.')):
return files = choose_files(self.gui, 'tweak-book-bulk-import-files', _('Choose files'))
if files:
folder_map = get_recommended_folders(current_container(), files)
files = {x:('/'.join((folder, os.path.basename(x))) if folder else os.path.basename(x))
for x, folder in folder_map.items()}
self.add_savepoint(_('Before: Add files'))
c = current_container()
added_fonts = set()
for path in sorted(files, key=numeric_sort_key):
name = files[path]
i = 0 while c.exists(name) or c.manifest_has_name(name) or c.has_name_case_insensitive(name):
i += 1 name, ext = name.rpartition('.')[0::2]
name = f'{name}_{i}.{ext}'
try:
with open(path, 'rb') as f:
c.add_file(name, f.read())
except Exception:
self.rewind_savepoint()
raise if name.rpartition('.')[2].lower() in ('ttf', 'otf', 'woff'):
added_fonts.add(name)
self.gui.file_list.build(c)
if c.opf_name in editors:
editors[c.opf_name].replace_data(c.raw_data(c.opf_name))
self.set_modified()
completion_worker().clear_caches('names')
if added_fonts:
from calibre.gui2.tweak_book.manage_fonts import show_font_face_rule_for_font_files show_font_face_rule_for_font_files(c, added_fonts, self.gui)

def add_cover(self):
if not self.ensure_book():
return d = AddCover(current_container(), self.gui)
d.import_requested.connect(self.do_add_file)
try:
if d.exec() == QDialog.DialogCode.Accepted and d.file_name is not None:
report = []
with BusyCursor():
self.add_savepoint(_('Before: Add cover'))
set_cover(current_container(), d.file_name, report.append, options={
'existing_image':True, 'keep_aspect':tprefs['add_cover_preserve_aspect_ratio']})
self.apply_container_update_to_gui()
finally:
d.import_requested.disconnect()

def ensure_book(self, msg=None):
msg = msg or _('No book is currently open. You must first open a book.')
if current_container() is None:
error_dialog(self.gui, _('No book open'), msg, show=True)
return False return True def edit_toc(self):
if not self.ensure_book(_('You must open a book before trying to edit the Table of Contents.')):
return self.add_savepoint(_('Before: Edit Table of Contents'))
self.__current_toc_editor = d = TOCEditor(title=self.current_metadata.title, parent=self.gui)
d.finished.connect(self.toc_edit_finished)
# Using d.exec() causes showing the webview to hide the dialog d.open()

def toc_edit_finished(self, retcode: int):
self.__current_toc_editor = None if retcode != QDialog.DialogCode.Accepted:
self.rewind_savepoint()
return with BusyCursor():
self.set_modified()
self.update_editors_from_container()
self.gui.toc_view.update_if_visible()
self.gui.file_list.build(current_container())

def insert_inline_toc(self):
if not self.ensure_book():
return self.commit_all_editors_to_container()
self.add_savepoint(_('Before: Insert inline Table of Contents'))
name = create_inline_toc(current_container())
if name is None:
self.rewind_savepoint()
return error_dialog(self.gui, _('No Table of Contents'), _(
'Cannot create an inline Table of Contents as this book has no existing'
' Table of Contents. You must first create a Table of Contents using the'
' Edit Table of Contents tool.'), show=True)
self.apply_container_update_to_gui()
self.edit_file(name, 'html')

def polish(self, action, name, parent=None):
if not self.ensure_book():
return from calibre.gui2.tweak_book.polish import get_customization, show_report customization = get_customization(action, name, parent or self.gui)
if customization is None:
return with BusyCursor():
self.add_savepoint(_('Before: %s') % name)
try:
report, changed = tweak_polish(current_container(), {action:True}, customization=customization)
except Exception:
self.rewind_savepoint()
raise if changed:
self.apply_container_update_to_gui()
self.gui.update_window_title()
if not changed:
self.rewind_savepoint()
show_report(changed, self.current_metadata.title, report, parent or self.gui, self.show_current_diff)

def transform_html(self):
global last_used_html_transform_rules if not self.ensure_book(_('You must first open a book in order to transform styles.')):
return from calibre.ebooks.html_transform_rules import transform_container from calibre.gui2.html_transform_rules import RulesDialog d = RulesDialog(self.gui)
d.rules = last_used_html_transform_rules d.transform_scope = tprefs['html_transform_scope']
ret = d.exec()
last_used_html_transform_rules = d.rules scope = d.transform_scope tprefs.set('html_transform_scope', scope)
if ret != QDialog.DialogCode.Accepted:
return mime_map = current_container().mime_map names = ()
if scope == 'current':
if not self.currently_editing or mime_map.get(self.currently_editing) not in OEB_DOCS:
return error_dialog(self.gui, _('No HTML file'), _('Not currently editing an HTML file'), show=True)
names = (self.currently_editing,)
elif scope == 'open':
names = tuple(name for name in editors if mime_map.get(name) in OEB_DOCS)
if not names:
return error_dialog(self.gui, _('No HTML files'), _('Not currently editing any HTML files'), show=True)
elif scope == 'selected':
names = tuple(name for name in self.gui.file_list.file_list.selected_names if mime_map.get(name) in OEB_DOCS)
if not names:
return error_dialog(self.gui, _('No HTML files'), _('No HTML files are currently selected in the File browser'), show=True)
with BusyCursor():
self.add_savepoint(_('Before HTML transformation'))
try:
changed = transform_container(current_container(), last_used_html_transform_rules, names)
except Exception:
self.rewind_savepoint()
raise if changed:
self.apply_container_update_to_gui()
if not changed:
self.rewind_savepoint()
return info_dialog(self.gui, _('No changes'), _('No HTML was changed.'), show=True)
self.show_current_diff()

def transform_styles(self):
global last_used_transform_rules if not self.ensure_book(_('You must first open a book in order to transform styles.')):
return from calibre.ebooks.css_transform_rules import transform_container from calibre.gui2.css_transform_rules import RulesDialog d = RulesDialog(self.gui)
d.rules = last_used_transform_rules ret = d.exec()
last_used_transform_rules = d.rules if ret != QDialog.DialogCode.Accepted:
return with BusyCursor():
self.add_savepoint(_('Before style transformation'))
try:
changed = transform_container(current_container(), last_used_transform_rules)
except Exception:
self.rewind_savepoint()
raise if changed:
self.apply_container_update_to_gui()
if not changed:
self.rewind_savepoint()
info_dialog(self.gui, _('No changes'), _(
'No styles were changed.'), show=True)
return self.show_current_diff()

def get_external_resources(self):
if not self.ensure_book(_('You must first open a book in order to transform styles.')):
return from calibre.gui2.tweak_book.download import DownloadResources with BusyCursor():
self.add_savepoint(_('Before: Get external resources'))
try:
d = DownloadResources(self.gui)
d.exec()
except Exception:
self.rewind_savepoint()
raise if d.resources_replaced:
self.apply_container_update_to_gui()
if d.show_diff:
self.show_current_diff()
else:
self.rewind_savepoint()

def manage_fonts(self):
if not self.ensure_book(_('No book is currently open. You must first open a book to manage fonts.')):
return self.commit_all_editors_to_container()
self.gui.manage_fonts.display()

def manage_fonts_embed(self):
self.polish('embed', _('Embed all fonts'), parent=self.gui.manage_fonts)
self.gui.manage_fonts.refresh()

def manage_fonts_subset(self):
self.polish('subset', _('Subset all fonts'), parent=self.gui.manage_fonts)

# Renaming {{{ def rationalize_folders(self):
if not self.ensure_book():
return c = current_container()
if not c.SUPPORTS_FILENAMES:
return error_dialog(self.gui, _('Not supported'),
_('The %s format does not support file and folder names internally, therefore'
' arranging files into folders is not allowed.') % c.book_type.upper(), show=True)
d = RationalizeFolders(self.gui)
if d.exec() != QDialog.DialogCode.Accepted:
return self.commit_all_editors_to_container()
name_map = rationalize_folders(c, d.folder_map)
if not name_map:
confirm(_(
'The files in this book are already arranged into folders'), 'already-arranged-into-folders', self.gui, pixmap='dialog_information.png', title=_('Nothing to do'), show_cancel_button=False, config_set=tprefs, confirm_msg=_('Show this message &again'))
return self.add_savepoint(_('Before: Arrange into folders'))
self.gui.blocking_job(
'rationalize_folders', _('Renaming and updating links...'), partial(self.rename_done, name_map), rename_files, current_container(), name_map)

def rename_requested(self, oldname, newname):
self.commit_all_editors_to_container()
if guess_type(oldname) != guess_type(newname):
args = os.path.splitext(oldname) + os.path.splitext(newname)
if not confirm(
_('You are changing the file type of {0}<b>{1}</b> to {2}<b>{3}</b>.'
' Doing so can cause problems, are you sure?').format(*args),
'confirm-filetype-change', parent=self.gui, title=_('Are you sure?'), config_set=tprefs):
return if urlnormalize(newname) != newname:
if not confirm(
_('The name you have chosen {0} contains special characters, internally'
' it will look like: {1}Try to use only the English alphabet [a-z], numbers [0-9],'
' hyphens and underscores for file names. Other characters can cause problems for '
' different e-book viewers. Are you sure you want to proceed?').format( f'<pre>{newname}</pre>', f'<pre>{urlnormalize(newname)}</pre>'),
'confirm-urlunsafe-change', parent=self.gui, title=_('Are you sure?'), config_set=tprefs):
return self.add_savepoint(_('Before: Rename %s') % oldname)
name_map = {oldname:newname}
self.gui.blocking_job(
'rename_file', _('Renaming and updating links...'), partial(self.rename_done, name_map, from_filelist=self.gui.file_list.current_name), rename_files, current_container(), name_map)

def bulk_rename_requested(self, name_map):
self.add_savepoint(_('Before: Bulk rename'))
self.gui.blocking_job(
'bulk_rename_files', _('Renaming and updating links...'), partial(self.rename_done, name_map, from_filelist=self.gui.file_list.current_name), rename_files, current_container(), name_map)

def rename_done(self, name_map, job, from_filelist=None):
if job.traceback is not None:
self.gui.file_list.restore_temp_names()
return error_dialog(self.gui, _('Failed to rename files'),
_('Failed to rename files, click "Show details" for more information.'), det_msg=job.traceback, show=True)
self.gui.file_list.build(current_container())
self.set_modified()
for oldname, newname in name_map.items():
if oldname in editors:
editors[newname] = ed = editors.pop(oldname)
ed.change_document_name(newname)
self.gui.central.rename_editor(editors[newname], newname)
if self.gui.preview.current_name == oldname:
self.gui.preview.current_name = newname self.apply_container_update_to_gui()
if from_filelist:
self.gui.file_list.select_names(frozenset(name_map.values()), current_name=name_map.get(from_filelist))
self.gui.file_list.file_list.setFocus(Qt.FocusReason.PopupFocusReason)

# }}}

# Global history {{{ def do_global_undo(self, *a):
container = self.global_undo.undo()
if container is not None:
set_current_container(container)
self.apply_container_update_to_gui()

def do_global_redo(self):
container = self.global_undo.redo()
if container is not None:
set_current_container(container)
self.apply_container_update_to_gui()

def update_global_history_actions(self):
gu = self.global_undo for x, text in (('undo', _('&Revert to')), ('redo', _('&Revert to'))):
ac = getattr(self.gui, f'action_global_{x}')
ac.setEnabled(getattr(gu, 'can_' + x))
ac.setText(text + ' "%s"'%(getattr(gu, x + '_msg') or '...'))

[문서]
def add_savepoint(self, msg):
' Create a restore checkpoint with the name specified as ``msg`` '
self.commit_all_editors_to_container()
nc = clone_container(current_container(), self.mkdtemp())
self.global_undo.add_savepoint(nc, msg)
set_current_container(nc)
self.update_global_history_actions()

[문서]
def rewind_savepoint(self):
' Undo the previous creation of a restore checkpoint, useful if you create a checkpoint, then abort the operation with no changes '
container = self.global_undo.rewind_savepoint()
if container is not None:
set_current_container(container)
self.update_global_history_actions()

def create_diff_dialog(self, revert_msg=_('&Revert changes'), show_open_in_editor=True):
global _diff_dialogs from calibre.gui2.tweak_book.diff.main import Diff def line_activated(name, lnum, right):
if right:
self.edit_file_requested(name, None, guess_type(name))
if name in editors:
editor = editors[name]
editor.go_to_line(lnum)
editor.setFocus(Qt.FocusReason.OtherFocusReason)
self.gui.raise_and_focus()
d = Diff(revert_button_msg=revert_msg, show_open_in_editor=show_open_in_editor)
[x.break_cycles() for x in _diff_dialogs if not x.isVisible()]
_diff_dialogs = [x for x in _diff_dialogs if x.isVisible()] + [d]
d.show(), d.raise_and_focus(), d.setFocus(Qt.FocusReason.OtherFocusReason), d.setWindowModality(Qt.WindowModality.NonModal)
if show_open_in_editor:
d.line_activated.connect(line_activated)
return d

[문서]
def show_current_diff(self, allow_revert=True, to_container=None):
'''
Show the changes to the book from its last checkpointed state

:param allow_revert: If True the diff dialog will have a button to allow the user to revert all changes
:param to_container: A container object to compare the current container to. If None, the previously checkpointed container is used
'''
self.commit_all_editors_to_container()
k = {} if allow_revert else {'revert_msg': None}
d = self.create_diff_dialog(**k)
previous_container = self.global_undo.previous_container connect_lambda(d.revert_requested, self, lambda self: self.revert_requested(previous_container))
other = to_container or self.global_undo.previous_container d.container_diff(other, self.global_undo.current_container, names=(self.global_undo.label_for_container(other), self.global_undo.label_for_container(self.global_undo.current_container)))

def ask_to_show_current_diff(self, name, title, msg, allow_revert=True, to_container=None):
if tprefs.get('skip_ask_to_show_current_diff_for_' + name):
return d = QDialog(self.gui)
k = QVBoxLayout(d)
d.setWindowTitle(title)
k.addWidget(QLabel(msg))
k.confirm = cb = QCheckBox(_('Show this popup again'))
k.addWidget(cb)
cb.setChecked(True)
connect_lambda(cb.toggled, d, lambda d, checked: tprefs.set('skip_ask_to_show_current_diff_for_' + name, not checked))
d.bb = bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Close, d)
k.addWidget(bb)
bb.accepted.connect(d.accept)
bb.rejected.connect(d.reject)
d.b = b = bb.addButton(_('See what &changed'), QDialogButtonBox.ButtonRole.AcceptRole)
b.setIcon(QIcon.ic('diff.png')), b.setAutoDefault(False)
bb.button(QDialogButtonBox.StandardButton.Close).setDefault(True)
if d.exec() == QDialog.DialogCode.Accepted:
self.show_current_diff(allow_revert=allow_revert, to_container=to_container)

def compare_book(self):
if not self.ensure_book():
return self.commit_all_editors_to_container()
c = current_container()
path = choose_files(self.gui, 'select-book-for-comparison', _('Choose book'), filters=[
(_('%s books') % c.book_type.upper(), (c.book_type,))], select_only_single_file=True, all_files=False)
if path and path[0]:
with TemporaryDirectory('_compare') as tdir:
other = _gc(path[0], tdir=tdir, tweak_mode=True)
d = self.create_diff_dialog(revert_msg=None)
d.container_diff(other, c, names=(_('Other book'), _('Current book')))

def revert_requested(self, container):
self.commit_all_editors_to_container()
nc = self.global_undo.revert_to(container)
set_current_container(nc)
self.apply_container_update_to_gui()

def compare_requested(self, container):
self.show_current_diff(to_container=container)

# }}}

[문서]
def set_modified(self):
' Mark the book as having been modified '
self.gui.action_save.setEnabled(True)

def request_completion(self, name, completion_type, completion_data, query=None):
if completion_type is None:
completion_worker().clear_caches(completion_data)
return request_id = (self.completion_request_count, name)
self.completion_request_count += 1 completion_worker().queue_completion(request_id, completion_type, completion_data, query)
return request_id[0]

def handle_completion_result(self, result):
name = result.request_id[1]
editor = editors.get(name)
if editor is not None:
editor.handle_completion_result(result)

def fix_html(self, current):
if current:
ed = self.gui.central.current_editor if hasattr(ed, 'fix_html'):
ed.fix_html()
else:
with BusyCursor():
self.add_savepoint(_('Before: Fix HTML'))
fix_all_html(current_container())
self.update_editors_from_container()
self.set_modified()
self.ask_to_show_current_diff('html-fix', _('Fixing done'), _('All HTML files fixed'))

def pretty_print(self, current):
if current:
ed = self.gui.central.current_editor ed.pretty_print(editor_name(ed))
else:
with BusyCursor():
self.add_savepoint(_('Before: Beautify files'))
pretty_all(current_container())
self.update_editors_from_container()
self.set_modified()
QApplication.alert(self.gui)
self.ask_to_show_current_diff('beautify', _('Beautified'), _('All files beautified'))

def mark_selected_text(self):
ed = self.gui.central.current_editor if ed is not None:
ed.mark_selected_text()
if ed.has_marked_text:
self.gui.central.search_panel.set_where('selected-text')
else:
self.gui.central.search_panel.unset_marked()

def editor_action(self, action):
ed = self.gui.central.current_editor edname = editor_name(ed)
if hasattr(ed, 'action_triggered'):
if action and action[0] == 'insert_resource':
rtype = action[1]
if rtype == 'image' and ed.syntax not in {'css', 'html'}:
return error_dialog(self.gui, _('Not supported'), _(
'Inserting images is only supported for HTML and CSS files.'), show=True)
rdata = get_resource_data(rtype, self.gui)
if rdata is None:
return if rtype == 'image':
chosen_name, chosen_image_is_external, fullpage, preserve_ar = rdata if chosen_image_is_external:
with open(chosen_image_is_external[1], 'rb') as f:
current_container().add_file(chosen_image_is_external[0], f.read())
self.refresh_file_list()
chosen_name = chosen_image_is_external[0]
href = current_container().name_to_href(chosen_name, edname)
fmt, width, height = identify(current_container().raw_data(chosen_name, decode=False))
ed.insert_image(href, fullpage=fullpage, preserve_aspect_ratio=preserve_ar, width=width, height=height)
elif action[0] == 'insert_hyperlink':
self.commit_all_editors_to_container()
d = InsertLink(current_container(), edname, initial_text=ed.get_smart_selection(), parent=self.gui)
if d.exec() == QDialog.DialogCode.Accepted:
ed.insert_hyperlink(d.href, d.text, template=d.rendered_template)
elif action[0] == 'insert_tag':
d = InsertTag(parent=self.gui)
if d.exec() == QDialog.DialogCode.Accepted:
ed.insert_tag(d.tag)
else:
ed.action_triggered(action)

def rename_class(self, class_name):
self.commit_all_editors_to_container()
text, ok = QInputDialog.getText(self.gui, _('New class name'), _(
'Rename the class {} to?').format(class_name))
if ok:
self.add_savepoint(_('Before: Rename {}').format(class_name))
with BusyCursor():
changed = rename_class(current_container(), class_name, text.strip())
if changed:
self.apply_container_update_to_gui()
self.show_current_diff()
else:
self.rewind_savepoint()
return info_dialog(self.gui, _('No matches'), _(
'No class {} found to change').format(class_name), show=True)

def set_semantics(self):
if not self.ensure_book():
return self.commit_all_editors_to_container()
c = current_container()
if c.book_type == 'azw3':
return error_dialog(self.gui, _('Not supported'), _(
'Semantics are not supported for the AZW3 format.'), show=True)
d = InsertSemantics(c, parent=self.gui)
if d.exec() == QDialog.DialogCode.Accepted and d.changes:
self.add_savepoint(_('Before: Set Semantics'))
d.apply_changes(current_container())
self.apply_container_update_to_gui()

def filter_css(self):
self.commit_all_editors_to_container()
c = current_container()
ed = self.gui.central.current_editor current_name = editor_name(ed)
if current_name and c.mime_map[current_name] not in OEB_DOCS | OEB_STYLES:
current_name = None d = FilterCSS(current_name=current_name, parent=self.gui)
if d.exec() == QDialog.DialogCode.Accepted and d.filtered_properties:
self.add_savepoint(_('Before: Filter style information'))
with BusyCursor():
changed = filter_css(current_container(), d.filtered_properties, names=d.filter_names)
if changed:
self.apply_container_update_to_gui()
self.show_current_diff()
else:
self.rewind_savepoint()
return info_dialog(self.gui, _('No matches'), _(
'No matching style rules were found'), show=True)

def show_find(self):
self.gui.central.show_find()
ed = self.gui.central.current_editor if ed is not None and hasattr(ed, 'selected_text'):
text = ed.selected_text if text and text.strip():
self.gui.central.pre_fill_search(text)

def show_text_search(self):
self.gui.text_search_dock.show()
self.gui.text_search.find.setFocus(Qt.FocusReason.OtherFocusReason)

def search_action_triggered(self, action, overrides=None):
ss = self.gui.saved_searches.isVisible()
trigger_saved_search = ss and (not self.gui.central.search_panel.isVisible() or self.gui.saved_searches.has_focus())
if trigger_saved_search:
return self.gui.saved_searches.trigger_action(action, overrides=overrides)
self.search(action, overrides)

def run_saved_searches(self, searches, action):
ed = self.gui.central.current_editor name = editor_name(ed)
searchable_names = self.gui.file_list.searchable_names if not searches or not validate_search_request(name, searchable_names, getattr(ed, 'has_marked_text', False), searches[0], self.gui):
return ret = run_search(searches, action, ed, name, searchable_names, self.gui, self.show_editor, self.edit_file, self.show_current_diff, self.add_savepoint, self.rewind_savepoint, self.set_modified)
ed = ret is True and self.gui.central.current_editor if getattr(ed, 'has_line_numbers', False):
ed.editor.setFocus(Qt.FocusReason.OtherFocusReason)
else:
self.gui.saved_searches.setFocus(Qt.FocusReason.OtherFocusReason)

def search(self, action, overrides=None):
# Run a search/replace sp = self.gui.central.search_panel
# Ensure the search panel is visible sp.setVisible(True)
ed = self.gui.central.current_editor name = editor_name(ed)
state = sp.state if overrides:
state.update(overrides)
searchable_names = self.gui.file_list.searchable_names if not validate_search_request(name, searchable_names, getattr(ed, 'has_marked_text', False), state, self.gui):
return ret = run_search(state, action, ed, name, searchable_names, self.gui, self.show_editor, self.edit_file, self.show_current_diff, self.add_savepoint, self.rewind_savepoint, self.set_modified)
ed = ret is True and self.gui.central.current_editor if getattr(ed, 'has_line_numbers', False):
ed.editor.setFocus(Qt.FocusReason.OtherFocusReason)
else:
self.gui.saved_searches.setFocus(Qt.FocusReason.OtherFocusReason)

def find_text(self, state):
from calibre.gui2.tweak_book.text_search import run_text_search searchable_names = self.gui.file_list.searchable_names ed = self.gui.central.current_editor name = editor_name(ed)
if not validate_search_request(name, searchable_names, getattr(ed, 'has_marked_text', False), state, self.gui):
return ret = run_text_search(state, ed, name, searchable_names, self.gui, self.show_editor, self.edit_file)
ed = ret is True and self.gui.central.current_editor if getattr(ed, 'has_line_numbers', False):
ed.editor.setFocus(Qt.FocusReason.OtherFocusReason)

def find_initial_text(self, text):
from calibre.gui2.tweak_book.search import get_search_regex from calibre.gui2.tweak_book.text_search import file_matches_pattern search = {'find': text, 'mode': 'normal', 'case_sensitive': True, 'direction': 'down'}
pat = get_search_regex(search)
searchable_names = set(self.gui.file_list.searchable_names['text'])
for name, ed in editors.items():
searchable_names.discard(name)
if ed.find_text(pat, complete=True):
self.show_editor(name)
return for name in searchable_names:
if file_matches_pattern(name, pat):
self.edit_file(name)
if editors[name].find_text(pat, complete=True):
return def find_word(self, word, locations):
# Go to a word from the spell check dialog ed = self.gui.central.current_editor name = editor_name(ed)
find_next_word(word, locations, ed, name, self.gui, self.show_editor, self.edit_file)

def next_spell_error(self):
# Go to the next spelling error ed = self.gui.central.current_editor name = editor_name(ed)
find_next_error(ed, name, self.gui, self.show_editor, self.edit_file, self.close_editor)

def word_change_requested(self, w, new_word):
if self.commit_all_editors_to_container():
self.gui.spell_check.change_word_after_update(w, new_word)
else:
self.gui.spell_check.do_change_word(w, new_word)

def word_replaced(self, changed_names):
self.set_modified()
self.update_editors_from_container(names=set(changed_names))

def word_ignored(self, word, locale):
if tprefs['inline_spell_check']:
for ed in editors.values():
try:
ed.editor.recheck_word(word, locale)
except AttributeError:
pass def editor_link_clicked(self, url):
ed = self.gui.central.current_editor name = editor_name(ed)
if url.startswith('#'):
target = name else:
target = current_container().href_to_name(url, name)
frag = url.partition('#')[-1]
if current_container().has_name(target):
self.link_clicked(target, frag, show_anchor_not_found=True)
else:
try:
purl = urlparse(url)
except ValueError:
return if purl.scheme not in {'', 'file'}:
open_url(QUrl(url))
else:
error_dialog(self.gui, _('Not found'), _(
'No file with the name %s was found in the book') % target, show=True)

def editor_class_clicked(self, class_data):
from calibre.gui2.tweak_book.jump_to_class import NoMatchingRuleFound, NoMatchingTagFound, find_first_matching_rule ed = self.gui.central.current_editor name = editor_name(ed)
try:
res = find_first_matching_rule(current_container(), name, ed.get_raw_data(), class_data)
except (NoMatchingTagFound, NoMatchingRuleFound):
res = None if res is not None and res.file_name and res.rule_address:
editor = self.open_editor_for_name(res.file_name)
if editor:
editor.goto_css_rule(res.rule_address, sourceline_address=res.style_tag_address)
else:
error_dialog(self.gui, _('No matches found'), _('No style rules that match the class {} were found').format( class_data['class']), show=True)

def save_search(self):
state = self.gui.central.search_panel.state self.show_saved_searches()
self.gui.saved_searches.add_predefined_search(state)

def show_saved_searches(self):
self.gui.saved_searches_dock.show()
saved_searches = show_saved_searches def create_checkpoint(self):
text, ok = QInputDialog.getText(self.gui, _('Choose name'), _(
'Choose a name for the checkpoint.\nYou can later restore the book'
' to this checkpoint via the\n"Revert to..." entries in the Edit menu.'))
if ok:
self.add_savepoint(text)

def commit_editor_to_container(self, name, container=None):
container = container or current_container()
ed = editors[name]
with container.open(name, 'wb') as f:
f.write(ed.data)
if name == container.opf_name:
container.refresh_mime_map()
lang = container.opf_xpath('//dc:language/text()') or [self.current_metadata.language]
set_book_locale(lang[0])
if container is current_container():
ed.is_synced_to_container = True if name == container.opf_name:
self.gui.file_list.build(container)

[문서]
def commit_all_editors_to_container(self):
''' Commit any changes that the user has made to files open in editors to the container. You should call this method before performing any actions on the current container '''
changed = False with BusyCursor():
for name, ed in editors.items():
if not ed.is_synced_to_container:
self.commit_editor_to_container(name)
ed.is_synced_to_container = True changed = True return changed

[문서]
def save_book(self):
' Save the book. Saving is performed in the background '
self.gui.update_window_title()
c = current_container()
for name, ed in editors.items():
if ed.is_modified or not ed.is_synced_to_container:
self.commit_editor_to_container(name, c)
ed.is_modified = False path_to_ebook = os.path.abspath(c.path_to_ebook)
destdir = os.path.dirname(path_to_ebook)
if not c.is_dir and not os.path.exists(destdir):
info_dialog(self.gui, _('Path does not exist'), _(
'The file you are editing (%s) no longer exists. You have to choose a new save location.') % path_to_ebook, show_copy_button=False, show=True)
fmt = path_to_ebook.rpartition('.')[-1].lower()
start_dir = find_first_existing_ancestor(path_to_ebook)
path = choose_save_file( self.gui, 'choose-new-save-location', _('Choose file location'), initial_path=os.path.join(start_dir, os.path.basename(path_to_ebook)), filters=[(fmt.upper(), (fmt,))], all_files=False)
if path is not None:
if not path.lower().endswith('.' + fmt):
path = path + '.' + fmt path = os.path.abspath(path)
c.path_to_ebook = path self.global_undo.update_path_to_ebook(path)
else:
return if os.path.exists(c.path_to_ebook) and not os.access(c.path_to_ebook, os.W_OK):
if not question_dialog(self.gui, _('File is read-only'), _(
'The file at {} is read-only. The editor will try to reset its permissions before saving. Proceed with saving?'
).format(c.path_to_ebook), override_icon='dialog_warning.png', yes_text=_('&Save'), no_text=_('&Cancel'), yes_icon='save.png'):
return self.gui.action_save.setEnabled(False)
tdir = self.mkdtemp(prefix='save-')
container = clone_container(c, tdir)
self.save_manager.schedule(tdir, container)def _save_copy(self, post_action=None):
self.gui.update_window_title()
c = current_container()
if c.is_dir:
return error_dialog(self.gui, _('Cannot save a copy'), _(
'Saving a copy of a folder based book is not supported'), show=True)
ext = c.path_to_ebook.rpartition('.')[-1]
path = choose_save_file( self.gui, 'tweak_book_save_copy', _('Choose path'), initial_filename=self.current_metadata.title + '.' + ext, filters=[(_('Book (%s)') % ext.upper(), [ext.lower()])], all_files=False)
if not path:
return if '.' not in os.path.basename(path):
path += '.' + ext.lower()
tdir = self.mkdtemp(prefix='save-copy-')
container = clone_container(c, tdir)
for name, ed in editors.items():
if ed.is_modified or not ed.is_synced_to_container:
self.commit_editor_to_container(name, container)

def do_save(c, path, tdir):
save_container(c, path)
shutil.rmtree(tdir, ignore_errors=True)
return path self.gui.blocking_job('save_copy', _('Saving copy, please wait...'), partial(self.copy_saved, post_action=post_action), do_save, container, path, tdir)

def save_copy(self):
self._save_copy()

def copy_saved(self, job, post_action=None):
if job.traceback is not None:
return error_dialog(self.gui, _('Failed to save copy'),
_('Failed to save copy, click "Show details" for more information.'), det_msg=job.traceback, show=True)
msg = _('Copy saved to %s') % job.result if post_action is None:
info_dialog(self.gui, _('Copy saved'), msg, show=True)
elif post_action == 'replace':
msg = _('Editing saved copy at: %s') % job.result self.open_book(job.result, edit_file=self.currently_editing)
elif post_action == 'edit':
if ismacos:
cmd = ['open', '-F', '-n', '-a', os.path.dirname(os.path.dirname(os.path.dirname(macos_edit_book_bundle_path()))), '--args']
else:
cmd = [exe_path('ebook-edit')]
if islinux:
cmd.append('--detach')
cmd.append(job.result)
ce = self.currently_editing if ce:
cmd.append(ce)
with sanitize_env_vars():
subprocess.Popen(cmd)
self.gui.show_status_message(msg, 5)

def report_save_error(self, tb):
if self.doing_terminal_save:
prints(tb, file=sys.stderr)
self.abort_terminal_save()
self.set_modified()
error_dialog(self.gui, _('Could not save'),
_('Saving of the book failed. Click "Show details"'
' for more information. You can try to save a copy'
' to a different location, via File->Save a copy'), det_msg=tb, show=True)

def go_to_line_number(self):
ed = self.gui.central.current_editor if ed is None or not ed.has_line_numbers:
return num, ok = QInputDialog.getInt(self.gui, _('Enter line number'), ('Line number:'), ed.current_line, 1, max(100000, ed.number_of_lines))
if ok:
ed.current_line = num def split_start_requested(self):
self.commit_all_editors_to_container()
self.gui.preview.do_start_split()

@in_thread_job def split_requested(self, name, loc, totals):
self.add_savepoint(_('Before: Split %s') % self.gui.elided_text(name))
try:
bottom_name = split(current_container(), name, loc, totals=totals)
except AbortError:
self.rewind_savepoint()
raise self.apply_container_update_to_gui()
self.edit_file(bottom_name, 'html')

def multisplit(self):
ed = self.gui.central.current_editor if ed.syntax != 'html':
return name = editor_name(ed)
if name is None:
return d = MultiSplit(self.gui)
if d.exec() == QDialog.DialogCode.Accepted:
with BusyCursor():
self.add_savepoint(_('Before: Split %s') % self.gui.elided_text(name))
try:
multisplit(current_container(), name, d.xpath)
except AbortError:
self.rewind_savepoint()
raise self.apply_container_update_to_gui()

def open_editor_for_name(self, name):
if name in editors:
editor = editors[name]
self.gui.central.show_editor(editor)
else:
try:
mt = current_container().mime_map[name]
except KeyError:
error_dialog(self.gui, _('Does not exist'), _(
'The file %s does not exist. If you were trying to click an item in'
' the Table of Contents, you may'
' need to refresh it by right-clicking and choosing "Refresh".') % name, show=True)
return None syntax = syntax_from_mime(name, mt)
if not syntax:
error_dialog( self.gui, _('Unsupported file format'),
_('Editing files of type %s is not supported') % mt, show=True)
return None editor = self.edit_file(name, syntax)
return editor def link_clicked(self, name, anchor, show_anchor_not_found=False):
if not name:
return editor = self.open_editor_for_name(name)
if anchor and hasattr(editor, 'go_to_anchor'):
if editor.go_to_anchor(anchor):
self.gui.preview.pending_go_to_anchor = anchor elif show_anchor_not_found:
error_dialog(self.gui, _('Not found'), _(
'The anchor %s was not found in this file') % anchor, show=True)

@in_thread_job def check_item_activated(self, item):
is_mult = item.has_multiple_locations and getattr(item, 'current_location_index', None) is not None name = item.all_locations[item.current_location_index][0] if is_mult else item.name editor = None if name in editors:
editor = editors[name]
self.gui.central.show_editor(editor)
else:
try:
editor = self.edit_file_requested(name, None, current_container().mime_map[name])
except KeyError:
error_dialog(self.gui, _('File deleted'), _(
'The file {} has already been deleted, re-run Check Book to update the results.').format(name), show=True)
if getattr(editor, 'has_line_numbers', False):
if is_mult:
editor.go_to_line(*(item.all_locations[item.current_location_index][1:3]))
else:
editor.go_to_line(item.line or 0, item.col or 0)
editor.set_focus()

@in_thread_job def check_requested(self, *args):
if current_container() is None:
return self.commit_all_editors_to_container()
c = self.gui.check_book c.parent().show()
c.parent().raise_and_focus()
c.run_checks(current_container())

def spell_check_requested(self):
if current_container() is None:
return self.commit_all_editors_to_container()
self.add_savepoint(_('Before: Spell Check'))
self.gui.spell_check.show()

@in_thread_job def fix_requested(self, errors):
self.add_savepoint(_('Before: Auto-fix errors'))
c = self.gui.check_book c.parent().show()
c.parent().raise_and_focus()
changed = c.fix_errors(current_container(), errors)
if changed:
self.apply_container_update_to_gui()
self.set_modified()
else:
self.rewind_savepoint()

@in_thread_job def merge_requested(self, category, names, master):
self.add_savepoint(_('Before: Merge files into %s') % self.gui.elided_text(master))
try:
merge(current_container(), category, names, master)
except AbortError:
self.rewind_savepoint()
raise self.apply_container_update_to_gui()
if master in editors:
self.show_editor(master)
self.gui.file_list.merge_completed(master)
self.gui.message_popup(_('{} files merged').format(len(names)))

@in_thread_job def link_stylesheets_requested(self, names, sheets, remove):
self.add_savepoint(_('Before: Link stylesheets'))
changed_names = link_stylesheets(current_container(), names, sheets, remove)
if changed_names:
self.update_editors_from_container(names=changed_names)
self.set_modified()

@in_thread_job def export_requested(self, name_or_names, path):
if isinstance(name_or_names, (str, bytes)):
return self.export_file(name_or_names, path)
for name in name_or_names:
dest = os.path.abspath(os.path.join(path, name))
if '/' in name or os.sep in name:
try:
os.makedirs(os.path.dirname(dest))
except OSError as err:
if err.errno != errno.EEXIST:
raise self.export_file(name, dest)

def open_file_with(self, file_name, fmt, entry):
if file_name in editors and not editors[file_name].is_synced_to_container:
self.commit_editor_to_container(file_name)
with current_container().open(file_name) as src:
tdir = PersistentTemporaryDirectory(suffix='-ee-ow')
with open(os.path.join(tdir, os.path.basename(file_name)), 'wb') as dest:
shutil.copyfileobj(src, dest)
from calibre.gui2.open_with import run_program run_program(entry, dest.name, self)
if question_dialog(self.gui, _('File opened'), _(
'When you are done editing {0} click "Import" to update'
' the file in the book or "Discard" to lose any changes.').format(file_name), yes_text=_('Import'), no_text=_('Discard')
):
self.add_savepoint(_('Before: Replace %s') % file_name)
with open(dest.name, 'rb') as src, current_container().open(file_name, 'wb') as cdest:
shutil.copyfileobj(src, cdest)
self.apply_container_update_to_gui()
try:
shutil.rmtree(tdir)
except Exception:
pass

@in_thread_job def copy_files_to_clipboard(self, names):
names = tuple(names)
for name in names:
if name in editors and not editors[name].is_synced_to_container:
self.commit_editor_to_container(name)
container = current_container()
md = QMimeData()
url_map = { name:container.get_file_path_for_processing(name, allow_modification=False)
for name in names
}
from calibre.gui2.dnd import set_urls_from_local_file_paths set_urls_from_local_file_paths(md, *url_map.values())
import json md.setData(FILE_COPY_MIME, as_bytes(json.dumps({ name: (url_map[name], container.mime_map.get(name)) for name in names
})))
QApplication.instance().clipboard().setMimeData(md)

@in_thread_job def paste_files_from_clipboard(self):
md = QApplication.instance().clipboard().mimeData()
if md.hasUrls() and md.hasFormat(FILE_COPY_MIME):
import json self.commit_all_editors_to_container()
name_map = json.loads(bytes(md.data(FILE_COPY_MIME)))
container = current_container()
for name, (path, mt) in name_map.items():
with open(path, 'rb') as f:
container.add_file(name, f.read(), media_type=mt, modify_name_if_needed=True)
self.apply_container_update_to_gui()

def export_file(self, name, path):
if name in editors and not editors[name].is_synced_to_container:
self.commit_editor_to_container(name)
with current_container().open(name, 'rb') as src, open(path, 'wb') as dest:
shutil.copyfileobj(src, dest)

@in_thread_job def replace_requested(self, name, path, basename, force_mt):
self.add_savepoint(_('Before: Replace %s') % name)
replace_file(current_container(), name, path, basename, force_mt)
self.apply_container_update_to_gui()

def browse_images(self):
self.gui.image_browser.refresh()
self.gui.image_browser.show()
self.gui.image_browser.raise_and_focus()

def show_reports(self):
if not self.ensure_book(_('You must first open a book in order to see the report.')):
return self.gui.reports.refresh()
self.gui.reports.show()
self.gui.reports.raise_and_focus()

def reports_edit_requested(self, name):
mt = current_container().mime_map.get(name, guess_type(name))
self.edit_file_requested(name, None, mt)

def image_activated(self, name):
mt = current_container().mime_map.get(name, guess_type(name))
self.edit_file_requested(name, None, mt)

def check_external_links(self):
if self.ensure_book(_('You must first open a book in order to check links.')):
self.commit_all_editors_to_container()
self.gui.check_external_links.show()

def embed_tts(self):
if not self.ensure_book(_('You must first open a book in order to add Text-to-speech narration.')):
return self.commit_all_editors_to_container()
from calibre.gui2.tweak_book.tts import TTSEmbed self.add_savepoint(_('Before: adding narration'))
d = TTSEmbed(current_container(), self.gui)
if d.exec() == QDialog.DialogCode.Accepted:
self.apply_container_update_to_gui()
self.show_current_diff()
else:
self.rewind_savepoint()

def compress_images(self):
if not self.ensure_book(_('You must first open a book in order to compress images.')):
return from calibre.gui2.tweak_book.polish import CompressImages, CompressImagesProgress, show_report d = CompressImages(self.gui)
if d.exec() == QDialog.DialogCode.Accepted:
with BusyCursor():
self.add_savepoint(_('Before: compress images'))
d = CompressImagesProgress(names=d.names, jpeg_quality=d.jpeg_quality, webp_quality=d.webp_quality, parent=self.gui)
if d.exec() != QDialog.DialogCode.Accepted:
self.rewind_savepoint()
return changed, report = d.result if changed is None and report:
self.rewind_savepoint()
return error_dialog(self.gui, _('Unexpected error'), _(
'Failed to compress images, click "Show details" for more information'), det_msg=report, show=True)
if changed:
self.apply_container_update_to_gui()
else:
self.rewind_savepoint()
show_report(changed, self.current_metadata.title, report, self.gui, self.show_current_diff)

def sync_editor_to_preview(self, name, sourceline_address):
editor = self.edit_file(name, 'html')
self.ignore_preview_to_editor_sync = True try:
editor.goto_sourceline(*sourceline_address)
finally:
self.ignore_preview_to_editor_sync = False def do_sync_preview_to_editor(self, wait_for_highlight_to_finish=False):
if self.ignore_preview_to_editor_sync:
return ed = self.gui.central.current_editor if ed is not None:
name = editor_name(ed)
if name is not None and getattr(ed, 'syntax', None) == 'html':
hl = getattr(ed, 'highlighter', None)
if wait_for_highlight_to_finish:
if getattr(hl, 'is_working', False):
QTimer.singleShot(75, self.sync_preview_to_editor_on_highlight_finish)
return ct = ed.current_tag()
self.gui.preview.sync_to_editor(name, ct)
if hl is not None and hl.is_working:
QTimer.singleShot(75, self.sync_preview_to_editor_on_highlight_finish)

[문서]
def sync_preview_to_editor(self):
' Sync the position of the preview panel to the current cursor position in the current editor '
self.do_sync_preview_to_editor()

def sync_preview_to_editor_on_highlight_finish(self):
self.do_sync_preview_to_editor(wait_for_highlight_to_finish=True)

def show_partial_cfi_in_editor(self, name, cfi):
editor = self.edit_file(name, 'html')
if not editor or not editor.has_line_numbers:
return False from calibre.ebooks.epub.cfi.parse import decode_cfi from calibre.ebooks.oeb.polish.parsing import parse root = parse( editor.get_raw_data(), decoder=lambda x: x.decode('utf-8'), line_numbers=True, linenumber_attribute='data-lnum')
node = decode_cfi(root, cfi)

def barename(x):
return x.tag.partition('}')[-1]

if node is not None:
lnum = node.get('data-lnum')
if lnum:
tags_before = []
for tag in root.xpath(f'//*[@data-lnum="{lnum}"]'):
tags_before.append(barename(tag))
if tag is node:
break else:
tags_before.append(barename(node))
lnum = int(lnum)
return editor.goto_sourceline(lnum, tags_before, attribute='id' if node.get('id') else None)
return False def goto_style_declaration(self, data):
name = data['name']
editor = self.edit_file(name, syntax=data['syntax'])
self.gui.live_css.navigate_to_declaration(data, editor)

def init_editor(self, name, editor, data=None, use_template=False):
editor.undo_redo_state_changed.connect(self.editor_undo_redo_state_changed)
editor.data_changed.connect(self.editor_data_changed)
editor.copy_available_state_changed.connect(self.editor_copy_available_state_changed)
editor.cursor_position_changed.connect(self.sync_preview_to_editor)
editor.cursor_position_changed.connect(self.update_cursor_position)
if hasattr(editor, 'word_ignored'):
editor.word_ignored.connect(self.word_ignored)
if hasattr(editor, 'link_clicked'):
editor.link_clicked.connect(self.editor_link_clicked)
if hasattr(editor, 'class_clicked'):
editor.class_clicked.connect(self.editor_class_clicked)
if hasattr(editor, 'rename_class'):
editor.rename_class.connect(self.rename_class)
if getattr(editor, 'syntax', None) == 'html':
editor.smart_highlighting_updated.connect(self.gui.live_css.sync_to_editor)
if hasattr(editor, 'set_request_completion'):
editor.set_request_completion(partial(self.request_completion, name), name)
if data is not None:
if use_template:
editor.init_from_template(data)
else:
editor.data = data editor.is_synced_to_container = True editor.modification_state_changed.connect(self.editor_modification_state_changed)
self.gui.central.add_editor(name, editor)

[문서]
def edit_file(self, name, syntax=None, use_template=None):
''' Open the file specified by name in an editor

:param syntax: The media type of the file, for example, ``'text/html'``. If not specified it is guessed from the file extension.
:param use_template: A template to initialize the opened editor with
'''
editor = editors.get(name, None)
if editor is None:
syntax = syntax or syntax_from_mime(name, guess_type(name))
if use_template is None:
data = current_container().raw_data(name)
if isbytestring(data) and syntax in {'html', 'css', 'text', 'xml', 'javascript'}:
try:
data = data.decode('utf-8')
except UnicodeDecodeError:
return error_dialog(self.gui, _('Cannot decode'), _(
'Cannot edit %s as it appears to be in an unknown character encoding') % name, show=True)
else:
data = use_template editor = editors[name] = editor_from_syntax(syntax, self.gui.editor_tabs)
self.init_editor(name, editor, data, use_template=bool(use_template))
if tprefs['pretty_print_on_open']:
editor.pretty_print(name)
self.show_editor(name)
return editor

[문서]
def show_editor(self, name):
' Show the editor that is editing the file specified by ``name`` '
self.gui.central.show_editor(editors[name])
editors[name].set_focus()

def edit_file_requested(self, name, syntax=None, mime=None):
if name in editors:
self.gui.central.show_editor(editors[name])
return editors[name]
mime = mime or current_container().mime_map.get(name, guess_type(name))
syntax = syntax or syntax_from_mime(name, mime)
if not syntax:
return error_dialog( self.gui, _('Unsupported file format'),
_('Editing files of type %s is not supported') % mime, show=True)
return self.edit_file(name, syntax)

def edit_next_file(self, backwards=False):
self.gui.file_list.edit_next_file(self.currently_editing, backwards)

def quick_open(self):
if not self.ensure_book(_('No book is currently open. You must first open a book to edit.')):
return c = current_container()
files = [name for name, mime in c.mime_map.items() if c.exists(name) and syntax_from_mime(name, mime) is not None]
d = QuickOpen(files, parent=self.gui)
if d.exec() == QDialog.DialogCode.Accepted and d.selected_result is not None:
self.edit_file_requested(d.selected_result, None, c.mime_map[d.selected_result])

# Editor basic controls {{{ def do_editor_undo(self):
ed = self.gui.central.current_editor if ed is not None:
ed.undo()

def do_editor_redo(self):
ed = self.gui.central.current_editor if ed is not None:
ed.redo()

def do_editor_copy(self):
ed = self.gui.central.current_editor if ed is not None:
ed.copy()

def do_editor_cut(self):
ed = self.gui.central.current_editor if ed is not None:
ed.cut()

def do_editor_paste(self):
ed = self.gui.central.current_editor if ed is not None:
ed.paste()

def toggle_line_wrapping_in_all_editors(self):
tprefs['editor_line_wrap'] ^= True yes = tprefs['editor_line_wrap']
for ed in editors.values():
if getattr(ed, 'editor', None) and hasattr(ed.editor, 'apply_line_wrap_mode'):
ed.editor.apply_line_wrap_mode(yes)

def editor_data_changed(self, editor):
self.gui.preview.start_refresh_timer()
for name, ed in editors.items():
if ed is editor:
self.gui.toc_view.start_refresh_timer(name)
break def editor_undo_redo_state_changed(self, *args):
self.apply_current_editor_state()

def editor_copy_available_state_changed(self, *args):
self.apply_current_editor_state()

def editor_modification_state_changed(self, is_modified):
self.apply_current_editor_state()
if is_modified:
self.set_modified()
# }}}

def apply_current_editor_state(self):
ed = self.gui.central.current_editor self.gui.cursor_position_widget.update_position()
if ed is not None:
actions['editor-undo'].setEnabled(ed.undo_available)
actions['editor-redo'].setEnabled(ed.redo_available)
actions['editor-copy'].setEnabled(ed.copy_available)
actions['editor-cut'].setEnabled(ed.cut_available)
actions['go-to-line-number'].setEnabled(ed.has_line_numbers)
actions['fix-html-current'].setEnabled(ed.syntax == 'html')
name = editor_name(ed)
mime = current_container().mime_map.get(name)
if name is not None and (getattr(ed, 'syntax', None) == 'html' or mime == 'image/svg+xml'):
if self.gui.preview.show(name):
# The file being displayed by the preview has changed.
# Set the preview's position to the current cursor
# position in the editor, in case the editors' cursor
# position has not changed, since the last time it was
# focused. This is not inefficient since multiple requests
# to sync are de-bounced with a 100 msec wait.
self.sync_preview_to_editor()
if name is not None:
self.gui.file_list.mark_name_as_current(name)
if ed.has_line_numbers:
self.gui.cursor_position_widget.update_position(*ed.cursor_position)
else:
actions['go-to-line-number'].setEnabled(False)
self.gui.file_list.clear_currently_edited_name()

def update_cursor_position(self):
ed = self.gui.central.current_editor if getattr(ed, 'has_line_numbers', False):
self.gui.cursor_position_widget.update_position(*ed.cursor_position)
else:
self.gui.cursor_position_widget.update_position()

def editor_close_requested(self, editor):
name = editor_name(editor)
if not name:
return if not editor.is_synced_to_container:
self.commit_editor_to_container(name)
self.close_editor(name)

[문서]
def close_editor(self, name):
' Close the editor that is editing the file specified by ``name`` '
editor = editors.pop(name)
self.gui.central.close_editor(editor)
editor.break_cycles()
if not editors or getattr(self.gui.central.current_editor, 'syntax', None) != 'html':
self.gui.preview.clear()
self.gui.live_css.clear()

def insert_character(self):
self.gui.insert_char.show()

def manage_snippets(self):
from calibre.gui2.tweak_book.editor.snippets import UserSnippets
UserSnippets(self.gui).exec()

def merge_files(self):
self.gui.file_list.merge_files()

# Shutdown {{{ def quit(self):
if self.doing_terminal_save:
return False if self.save_manager.has_tasks:
if question_dialog( self.gui, _('Are you sure?'), _(
'The current book is being saved in the background. Quitting now will'
' <b>abort the save process</b>! Finish saving first?'), yes_text=_('Finish &saving first'), no_text=_('&Quit immediately')):
if self.save_manager.has_tasks:
self.start_terminal_save_indicator()
return False if not self.confirm_quit():
return False self.shutdown()
QApplication.instance().exit()
return True def confirm_quit(self):
if self.gui.action_save.isEnabled():
d = QDialog(self.gui)
d.l = QGridLayout(d)
d.setLayout(d.l)
d.setWindowTitle(_('Unsaved changes'))
d.i = QLabel('')
d.i.setMaximumSize(QSize(64, 64))
d.i.setPixmap(QIcon.ic('dialog_warning.png').pixmap(d.i.maximumSize()))
d.l.addWidget(d.i, 0, 0)
d.m = QLabel(_('There are unsaved changes, if you quit without saving, you will lose them.'))
d.m.setWordWrap(True)
d.l.addWidget(d.m, 0, 1)
d.bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Cancel)
d.bb.rejected.connect(d.reject)
d.bb.accepted.connect(d.accept)
d.l.addWidget(d.bb, 1, 0, 1, 2)
d.do_save = None def endit(d, x):
d.do_save = x d.accept()
b = d.bb.addButton(_('&Save and Quit'), QDialogButtonBox.ButtonRole.ActionRole)
b.setIcon(QIcon.ic('save.png'))
connect_lambda(b.clicked, d, lambda d: endit(d, True))
b = d.bb.addButton(_('&Quit without saving'), QDialogButtonBox.ButtonRole.ActionRole)
connect_lambda(b.clicked, d, lambda d: endit(d, False))
d.resize(d.sizeHint())
if d.exec() != QDialog.DialogCode.Accepted or d.do_save is None:
return False if d.do_save:
self.gui.action_save.trigger()
self.start_terminal_save_indicator()
return False return True def start_terminal_save_indicator(self):
self.save_state()
self.gui.blocking_job.set_msg(_('Saving, please wait...'))
self.gui.blocking_job.start()
self.doing_terminal_save = True def abort_terminal_save(self):
self.doing_terminal_save = False self.gui.blocking_job.stop()

def check_terminal_save(self):
if self.doing_terminal_save and not self.save_manager.has_tasks: # terminal save could have been aborted self.shutdown()
QApplication.instance().exit()

def shutdown(self):
self.save_state()
completion_worker().shutdown()
self.save_manager.check_for_completion.disconnect()
self.gui.preview.stop_refresh_timer()
self.gui.live_css.stop_update_timer()
[x.reject() for x in _diff_dialogs]
del _diff_dialogs[:]
self.save_manager.shutdown()
parse_worker.shutdown()
self.save_manager.wait(0.1)

def save_state(self):
with self.editor_cache:
self.save_book_edit_state()
with tprefs:
self.gui.save_state()

def save_book_edit_state(self):
c = current_container()
if c and c.path_to_ebook:
tprefs = self.editor_cache mem = tprefs['edit_book_state']
order = tprefs['edit_book_state_order']
extra = len(order) - 99 if extra > 0:
order = [k for k in order[extra:] if k in mem]
mem = {k:mem[k] for k in order}
mem[c.path_to_ebook] = {
'editors':{name:ed.current_editing_state for name, ed in editors.items()},
'currently_editing':self.currently_editing,
'tab_order':self.gui.central.tab_order,
}
try:
order.remove(c.path_to_ebook)
except ValueError:
pass order.append(c.path_to_ebook)
tprefs['edit_book_state'] = mem tprefs['edit_book_state_order'] = order def restore_book_edit_state(self):
c = current_container()
if c and c.path_to_ebook:
tprefs = self.editor_cache state = tprefs['edit_book_state'].get(c.path_to_ebook)
if state is not None:
opened = set()
eds = state.get('editors', {})
for name in state.get('tab_order', ()):
if c.has_name(name):
try:
editor = self.edit_file_requested(name)
if editor is not None:
opened.add(name)
es = eds.get(name)
if es is not None:
editor.current_editing_state = es except Exception:
import traceback traceback.print_exc()
ce = state.get('currently_editing')
if ce in opened:
self.show_editor(ce)# }}}

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 시작 »
- 모듈 코드 »
- calibre.gui2.tweak_book.plugin

calibre.gui2.tweak_book.plugin의 소스 코드

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'

import importlib import sys from qt.core import QToolButton from calibre import prints from calibre.customize import PluginInstallationType from calibre.customize.ui import all_edit_book_tool_plugins from calibre.gui2.tweak_book import current_container, tprefs from calibre.gui2.tweak_book.boss import get_boss

[문서]
class Tool:
'''
The base class for individual tools in an Edit Book plugin. Useful members include:

* ``self.plugin``: A reference to the :class:`calibre.customize.Plugin` object to which this tool belongs.
* self. :attr:`boss`
* self. :attr:`gui`

Methods that must be overridden in sub classes:

* :meth:`create_action`
* :meth:`register_shortcut`

'''

#: Set this to a unique name it will be used as a key name = None

#: If True the user can choose to place this tool in the plugins toolbar allowed_in_toolbar = True

#: If True the user can choose to place this tool in the plugins menu allowed_in_menu = True

#: The popup mode for the menu (if any) of the toolbar button. Possible values are 'delayed', 'instant', 'button'
toolbar_button_popup_mode = 'delayed'

@property def boss(self):
' The :class:`calibre.gui2.tweak_book.boss.Boss` object. Used to control the user interface. '
return get_boss()

@property def gui(self):
' The main window of the user interface '
return self.boss.gui

@property def current_container(self):
' Return the current :class:`calibre.ebooks.oeb.polish.container.Container` object that represents the book being edited. '
return current_container()

[문서]
def register_shortcut(self, qaction, unique_name, default_keys=(), short_text=None, description=None, **extra_data):
'''
Register a keyboard shortcut that will trigger the specified ``qaction``. This keyboard shortcut will become automatically customizable by the user in the Keyboard shortcuts section of the editor preferences.

:param qaction: A QAction object, it will be triggered when the configured key combination is pressed by the user.
:param unique_name: A unique name for this shortcut/action. It will be used internally, it must not be shared by any other actions in this plugin.
:param default_keys: A list of the default keyboard shortcuts. If not specified no default shortcuts will be set. If the shortcuts specified here conflict with either builtin shortcuts or shortcuts from user configuration/other plugins, they will be ignored. In that case, users will have to configure the shortcuts manually via Preferences. For example:
``default_keys=('Ctrl+J', 'F9')``.
:param short_text: An optional short description of this action. If not specified the text from the QAction will be used.
:param description: An optional longer description of this action, it will be used in the preferences entry for this shortcut.
'''
short_text = short_text or str(qaction.text()).replace('&&', '\0').replace('&', '').replace('\0', '&')
self.gui.keyboard.register_shortcut( self.name + '_' + unique_name, short_text, default_keys=default_keys, action=qaction, description=description or '', group=_('Plugins'))

[문서]
def create_action(self, for_toolbar=True):
'''
Create a QAction that will be added to either the plugins toolbar or the plugins menu depending on ``for_toolbar``. For example::

def create_action(self, for_toolbar=True):
ac = QAction(get_icons('myicon.png'), 'Do something')
if for_toolbar:
# We want the toolbar button to have a popup menu menu = QMenu()
ac.setMenu(menu)
menu.addAction('Do something else')
subaction = menu.addAction('And another')

# Register a keyboard shortcut for this toolbar action be
# careful to do this for only one of the toolbar action or
# the menu action, not both.
self.register_shortcut(ac, 'some-unique-name', default_keys=('Ctrl+K',))
return ac

.. seealso:: Method :meth:`register_shortcut`.
'''
raise NotImplementedError()

def load_plugin_tools(plugin):
try:
main = importlib.import_module(plugin.__class__.__module__+'.main')
except ImportError:
import traceback traceback.print_exc()
else:
for x in vars(main).values():
if isinstance(x, type) and x is not Tool and issubclass(x, Tool):
ans = x()
ans.plugin = plugin yield ans def plugin_action_sid(plugin, tool, for_toolbar=True):
return plugin.name + tool.name + ('toolbar' if for_toolbar else 'menu')

plugin_toolbar_actions = []

def create_plugin_action(plugin, tool, for_toolbar, actions=None, toolbar_actions=None, plugin_menu_actions=None):
try:
ac = tool.create_action(for_toolbar=for_toolbar)
if ac is None:
raise RuntimeError('create_action() failed to return an action')
except Exception:
prints('Failed to create action for tool:', tool.name)
import traceback traceback.print_exc()
return sid = plugin_action_sid(plugin, tool, for_toolbar)
if actions is not None and sid in actions:
prints(f'The {tool.name} tool from the {plugin.name} plugin has a non unique name, ignoring')
else:
if actions is not None:
actions[sid] = ac ac.sid = sid if for_toolbar:
if toolbar_actions is not None:
toolbar_actions[sid] = ac plugin_toolbar_actions.append(ac)
ac.popup_mode = {'instant':QToolButton.ToolButtonPopupMode.InstantPopup, 'button':QToolButton.ToolButtonPopupMode.MenuButtonPopup}.get( tool.toolbar_button_popup_mode, QToolButton.ToolButtonPopupMode.DelayedPopup)
elif plugin_menu_actions is not None:
plugin_menu_actions.append(ac)
return ac

_tool_memory = [] # Needed to prevent the tool object from being garbage collected def create_plugin_actions(actions, toolbar_actions, plugin_menu_actions):
del _tool_memory[:]
del plugin_toolbar_actions[:]

for plugin in all_edit_book_tool_plugins():
try:
tools = tuple(load_plugin_tools(plugin))
except Exception:
if plugin.installation_type is PluginInstallationType.BUILTIN:
raise print('Failed to load third-party plugin:', plugin.name, file=sys.stderr)
import traceback traceback.print_exc()
continue for tool in tools:
_tool_memory.append(tool)
if tool.allowed_in_toolbar:
create_plugin_action(plugin, tool, True, actions, toolbar_actions, plugin_menu_actions)
if tool.allowed_in_menu:
create_plugin_action(plugin, tool, False, actions, toolbar_actions, plugin_menu_actions)

def install_plugin(plugin):
for tool in load_plugin_tools(plugin):
if tool.allowed_in_toolbar:
sid = plugin_action_sid(plugin, tool, True)
if sid not in tprefs['global_plugins_toolbar']:
tprefs['global_plugins_toolbar'] = tprefs['global_plugins_toolbar'] + [sid]

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 시작 »
- 모듈 코드 »
- calibre.utils.formatter_functions

calibre.utils.formatter_functions의 소스 코드

#!/usr/bin/env python

'''
Created on 13 Jan 2011

@author: charles
'''

__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import inspect import numbers import os.path import posixpath import re import traceback from contextlib import suppress from datetime import datetime, timedelta from enum import Enum, auto from functools import partial from math import ceil, floor, modf, trunc from calibre import human_readable, prepare_string_for_xml, prints from calibre.constants import DEBUG from calibre.db.constants import DATA_DIR_NAME, DATA_FILE_PATTERN from calibre.ebooks.metadata import title_sort from calibre.ebooks.metadata.book.base import field_metadata from calibre.ebooks.metadata.search_internet import qquote from calibre.utils.config import tweaks from calibre.utils.date import UNDEFINED_DATE, format_date, now, parse_date from calibre.utils.icu import capitalize, sort_key, strcmp from calibre.utils.icu import lower as icu_lower from calibre.utils.localization import _ as xlated from calibre.utils.localization import calibre_langcode_to_name, canonicalize_lang from calibre.utils.titlecase import titlecase

UNKNOWN = _('Unknown')
RELATIONAL = _('Relational')
STRING_MANIPULATION = _('String manipulation')
IF_THEN_ELSE = _('If-then-else')
ARITHMETIC = _('Arithmetic')
RECURSION = _('Recursion')
OTHER = _('Other')
LIST_MANIPULATION = _('List manipulation')
LIST_LOOKUP = _('List lookup')
GET_FROM_METADATA = _('Get values from metadata')
ITERATING_VALUES = _('Iterate over values')
BOOLEAN = _('Boolean')
FORMATTING_VALUES = _('Formatting values')
CASE_CHANGES = _('Case changes')
DATE_FUNCTIONS = _('Date functions')
DB_FUNCS = _('Database functions')
URL_FUNCTIONS = _('URL functions')
GUI_FUNCTIONS = _('GUI functions')

# Class and method to save an untranslated copy of translated strings class TranslatedStringWithRaw(str):

def __new__(cls, raw_english, raw_other, formatted_english, formatted_other, msgid):
instance = super().__new__(cls, formatted_other)
instance.raw_english = raw_english instance.raw_other = raw_other instance.formatted_english = formatted_english instance.formatted_other = formatted_other instance.msgid = msgid instance.did_format = False return instance def format(self, *args, **kw):
formatted_english = self.raw_english.format(*args, **kw)
formatted_other = self.raw_other.format(*args, **kw)
v = TranslatedStringWithRaw(self.raw_english, self.raw_other, formatted_english, formatted_other, self.msgid)
v.saved_args = args v.saved_kwargs = kw v.did_format = True return v def format_again(self, txt):
if self.did_format:
return txt.format(*self.saved_args, **self.saved_kwargs)
return txt def translate_ffml(txt):
from calibre.utils.ffml_processor import FFMLProcessor msgid = FFMLProcessor().document_to_transifex(txt, '', safe=True).strip()
translated = xlated(msgid)
if translated == msgid:
translated = txt return TranslatedStringWithRaw(txt, translated, txt, translated, msgid)

class StoredObjectType(Enum):
PythonFunction = auto()
StoredGPMTemplate = auto()
StoredPythonTemplate = auto()

class FormatterFunctions:

error_function_body = ('def evaluate(self, formatter, kwargs, mi, locals):\n'
'\treturn "' +
_('Duplicate user function name {0}. '
'Change the name or ensure that the functions are identical') + '"')

def __init__(self):
self._builtins = {}
self._functions = {}
self._functions_from_library = {}

def register_builtin(self, func_class):
if not isinstance(func_class, FormatterFunction):
raise ValueError(f'Class {func_class.__class__.__name__} is not an instance of FormatterFunction')
name = func_class.name if name in self._functions:
raise ValueError(f'Name {name} already used')
self._builtins[name] = func_class self._functions[name] = func_class for a in func_class.aliases:
self._functions[a] = func_class def _register_function(self, func_class, replace=False):
if not isinstance(func_class, FormatterFunction):
raise ValueError(f'Class {func_class.__class__.__name__} is not an instance of FormatterFunction')
name = func_class.name if not replace and name in self._functions:
raise ValueError(f'Name {name} already used')
self._functions[name] = func_class def register_functions(self, library_uuid, funcs):
self._functions_from_library[library_uuid] = funcs self._register_functions()

def _register_functions(self):
for compiled_funcs in self._functions_from_library.values():
for cls in compiled_funcs:
f = self._functions.get(cls.name, None)
replace = False if f is not None:
existing_body = f.program_text new_body = cls.program_text if new_body != existing_body:
# Change the body of the template function to one that will
# return an error message. Also change the arg count to
# -1 (variable) to avoid template compilation errors if DEBUG:
print(f'attempt to replace formatter function {f.name} with a different body')
replace = True func = [cls.name, '', -1, self.error_function_body.format(cls.name)]
cls = compile_user_function(*func)
else:
continue formatter_functions()._register_function(cls, replace=replace)

def unregister_functions(self, library_uuid):
if library_uuid in self._functions_from_library:
for cls in self._functions_from_library[library_uuid]:
self._functions.pop(cls.name, None)
self._functions_from_library.pop(library_uuid)
self._register_functions()

def get_builtins(self):
return self._builtins def get_builtins_and_aliases(self):
res = {}
for f in self._builtins.values():
res[f.name] = f for a in f.aliases:
res[a] = f return res def get_functions(self):
return self._functions def reset_to_builtins(self):
self._functions = {}
for n,c in self._builtins.items():
self._functions[n] = c for a in c.aliases:
self._functions[a] = c

_ff = FormatterFunctions()

def formatter_functions():
global _ff return _ff def only_in_gui_error(name):
raise ValueError(_('The function {} can be used only in the GUI').format(name))

def get_database(mi, name):
try:
proxy = mi.get('_proxy_metadata', None)
except Exception:
proxy = None if proxy is None:
if name is not None:
only_in_gui_error(name)
return None wr = proxy.get('_db', None)
if wr is None:
if name is not None:
raise ValueError(_('In function {}: The database has been closed').format(name))
return None cache = wr()
if cache is None:
if name is not None:
raise ValueError(_('In function {}: The database has been closed').format(name))
return None wr = getattr(cache, 'database_instance', None)
if wr is None:
if name is not None:
only_in_gui_error(name)
return None db = wr()
if db is None:
if name is not None:
raise ValueError(_('In function {}: The database has been closed').format(name))
return None return db class FormatterFunction:

name = 'no name provided'
category = UNKNOWN arg_count = 0 aliases = []
object_type = StoredObjectType.PythonFunction
_cached_program_text = None def __doc__getter__(self) -> str:
return _('No documentation provided')

@property def doc(self):
return self.__doc__getter__()

@property def __doc__(self):
return self.__doc__getter__()

@property def program_text(self) -> str:
if self._cached_program_text is None:
eval_func = inspect.getmembers(self.__class__, lambda x: inspect.isfunction(x) and x.__name__ == 'evaluate')
try:
lines = [l[4:] for l in inspect.getsourcelines(eval_func[0][1])[0]]
except Exception:
lines = []
self._cached_program_text = ''.join(lines)
return self._cached_program_text def evaluate(self, formatter, kwargs, mi, locals, *args):
raise NotImplementedError()

def eval_(self, formatter, kwargs, mi, locals, *args):
ret = self.evaluate(formatter, kwargs, mi, locals, *args)
if isinstance(ret, (bytes, str)):
return ret if isinstance(ret, list):
return ','.join(ret)
if isinstance(ret, (numbers.Number, bool)):
return str(ret)

def only_in_gui_error(self):
only_in_gui_error(self.name)

def get_database(self, mi, formatter=None):
# Prefer the db that comes from proxy_metadata because it is probably an
# instance of LibraryDatabase where the one in the formatter might be an
# instance of Cache formatter_db = getattr(formatter, 'database', None)
if formatter_db is None:
# The formatter doesn't have a database. Try to get one from
# proxy_metadata. This will raise an exception because the name
# parameter is not None return get_database(mi, self.name)
else:
# We have a formatter db. Try to get the db from proxy_metadata but
# don't raise an exception if one isn't available.
legacy_db = get_database(mi, None)
return legacy_db if legacy_db is not None else formatter_db class BuiltinFormatterFunction(FormatterFunction):

def __init_subclass__(cls):
formatter_functions().register_builtin(cls())

[문서]
class BuiltinStrcmp(BuiltinFormatterFunction):
name = 'strcmp'
arg_count = 5 category = RELATIONAL def __doc__getter__(self): return translate_ffml( r'''
``strcmp(x, y, lt, eq, gt)`` -- does a case-insensitive lexical comparison of
``x`` and ``y``.[/] Returns ``lt`` if ``x < y``, ``eq`` if ``x == y``, otherwise
``gt``. This function can often be replaced by one of the lexical comparison operators (``==``, ``>``, ``<``, etc.)
''')

def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt):
v = strcmp(x, y)
if v < 0:
return lt if v == 0:
return eq return gt

[문서]
class BuiltinStrcmpcase(BuiltinFormatterFunction):
name = 'strcmpcase'
arg_count = 5 category = RELATIONAL def __doc__getter__(self): return translate_ffml( r'''
``strcmpcase(x, y, lt, eq, gt)`` -- does a case-sensitive lexical comparison of
``x`` and ``y``.[/] Returns ``lt`` if ``x < y``, ``eq`` if ``x == y``, otherwise
``gt``.

Note: This is NOT the default behavior used by calibre, for example, in the lexical comparison operators (``==``, ``>``, ``<``, etc.). This function could cause unexpected results, preferably use ``strcmp()`` whenever possible.
''')

def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt):
from calibre.utils.icu import case_sensitive_strcmp as case_strcmp v = case_strcmp(x, y)
if v < 0:
return lt if v == 0:
return eq return gt

[문서]
class BuiltinCmp(BuiltinFormatterFunction):
name = 'cmp'
category = RELATIONAL arg_count = 5 def __doc__getter__(self): return translate_ffml( r'''
``cmp(value, y, lt, eq, gt)`` -- compares ``value`` and ``y`` after converting both to numbers.[/] Returns ``lt`` if ``value <# y``, ``eq`` if ``value ==# y``, otherwise ``gt``.
This function can usually be replaced with one of the numeric compare operators
(``==#``, ``<#``, ``>#``, etc).
''')

def evaluate(self, formatter, kwargs, mi, locals, value, y, lt, eq, gt):
value = float(value if value and value != 'None' else 0)
y = float(y if y and y != 'None' else 0)
if value < y:
return lt if value == y:
return eq return gt

[문서]
class BuiltinFirstMatchingCmp(BuiltinFormatterFunction):
name = 'first_matching_cmp'
category = RELATIONAL arg_count = -1 def __doc__getter__(self): return translate_ffml( r'''
``first_matching_cmp(val, [ cmp, result, ]* else_result)`` -- compares ``val < cmp`` in sequence, returning the associated ``result`` for the first comparison that succeeds.[/] Returns ``else_result`` if no comparison succeeds.

Example:
[CODE]
i = 10; first_matching_cmp(i,5,"small",10,"middle",15,"large","giant")
[/CODE]
returns ``"large"``. The same example with a first value of 16 returns ``"giant"``.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
if (len(args) % 2) != 0:
raise ValueError(_('first_matching_cmp requires an even number of arguments'))
val = float(args[0] if args[0] and args[0] != 'None' else 0)
for i in range(1, len(args) - 1, 2):
c = float(args[i] if args[i] and args[i] != 'None' else 0)
if val < c:
return args[i+1]
return args[len(args)-1]

[문서]
class BuiltinStrcat(BuiltinFormatterFunction):
name = 'strcat'
arg_count = -1 category = STRING_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``strcat(a [, b]*)`` -- returns a string formed by concatenating all the arguments.[/] Can take any number of arguments. In most cases you can use the
``&`` operator instead of this function.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
i = 0 res = ''
for i in range(len(args)):
res += args[i]
return res

[문서]
class BuiltinStrlen(BuiltinFormatterFunction):
name = 'strlen'
arg_count = 1 category = STRING_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``strlen(value)`` -- Returns the length of the string ``value``.
''')

def evaluate(self, formatter, kwargs, mi, locals, a):
try:
return len(a)
except Exception:
return -1

[문서]
class BuiltinAdd(BuiltinFormatterFunction):
name = 'add'
arg_count = -1 category = ARITHMETIC def __doc__getter__(self): return translate_ffml(
'''
``add(x [, y]*)`` -- returns the sum of its arguments.[/] Throws an exception if an argument is not a number. In most cases you can use the ``+`` operator instead of this function.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
res = 0 for v in args:
v = float(v if v and v != 'None' else 0)
res += v return str(res)

[문서]
class BuiltinSubtract(BuiltinFormatterFunction):
name = 'subtract'
arg_count = 2 category = ARITHMETIC def __doc__getter__(self): return translate_ffml( r'''
``subtract(x, y)`` -- returns ``x - y``.[/] Throws an exception if either ``x`` or
``y`` are not numbers. This function can usually be replaced by the ``-`` operator.
''')

def evaluate(self, formatter, kwargs, mi, locals, x, y):
x = float(x if x and x != 'None' else 0)
y = float(y if y and y != 'None' else 0)
return str(x - y)

[문서]
class BuiltinMultiply(BuiltinFormatterFunction):
name = 'multiply'
arg_count = -1 category = ARITHMETIC def __doc__getter__(self): return translate_ffml( r'''
``multiply(x [, y]*)`` -- returns the product of its arguments.[/] Throws an exception if any argument is not a number. This function can usually be replaced by the ``*`` operator.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
res = 1 for v in args:
v = float(v if v and v != 'None' else 0)
res *= v return str(res)

[문서]
class BuiltinDivide(BuiltinFormatterFunction):
name = 'divide'
arg_count = 2 category = ARITHMETIC def __doc__getter__(self): return translate_ffml( r'''
``divide(x, y)`` -- returns ``x / y``.[/] Throws an exception if either ``x`` or
``y`` are not numbers. This function can usually be replaced by the ``/`` operator.
''')

def evaluate(self, formatter, kwargs, mi, locals, x, y):
x = float(x if x and x != 'None' else 0)
y = float(y if y and y != 'None' else 0)
return str(x / y)

[문서]
class BuiltinCeiling(BuiltinFormatterFunction):
name = 'ceiling'
arg_count = 1 category = ARITHMETIC def __doc__getter__(self): return translate_ffml( r'''
``ceiling(value)`` -- returns the smallest integer greater than or equal to ``value``.[/]
Throws an exception if ``value`` is not a number.
''')

def evaluate(self, formatter, kwargs, mi, locals, value):
value = float(value if value and value != 'None' else 0)
return str(ceil(value))

[문서]
class BuiltinFloor(BuiltinFormatterFunction):
name = 'floor'
arg_count = 1 category = ARITHMETIC def __doc__getter__(self): return translate_ffml( r'''
``floor(value)`` -- returns the largest integer less than or equal to ``value``.[/] Throws an exception if ``value`` is not a number.
''')

def evaluate(self, formatter, kwargs, mi, locals, value):
value = float(value if value and value != 'None' else 0)
return str(floor(value))

[문서]
class BuiltinRound(BuiltinFormatterFunction):
name = 'round'
arg_count = 1 category = ARITHMETIC def __doc__getter__(self): return translate_ffml( r'''
``round(value)`` -- returns the nearest integer to ``value``.[/] Throws an exception if
``value`` is not a number.
''')

def evaluate(self, formatter, kwargs, mi, locals, value):
value = float(value if value and value != 'None' else 0)
return str(round(value))

[문서]
class BuiltinMod(BuiltinFormatterFunction):
name = 'mod'
arg_count = 2 category = ARITHMETIC def __doc__getter__(self): return translate_ffml( r'''
``mod(value, y)`` -- returns the ``floor`` of the remainder of ``value / y``.[/] Throws an exception if either ``value`` or ``y`` is not a number.
''')

def evaluate(self, formatter, kwargs, mi, locals, value, y):
value = float(value if value and value != 'None' else 0)
y = float(y if y and y != 'None' else 0)
return str(int(value % y))

[문서]
class BuiltinFractionalPart(BuiltinFormatterFunction):
name = 'fractional_part'
arg_count = 1 category = ARITHMETIC def __doc__getter__(self): return translate_ffml( r'''
``fractional_part(value)`` -- returns the part of the value after the decimal point.[/] For example, ``fractional_part(3.14)`` returns ``0.14``. Throws an exception if ``value`` is not a number.
''')

def evaluate(self, formatter, kwargs, mi, locals, value):
value = float(value if value and value != 'None' else 0)
return str(modf(value)[0])

[문서]
class BuiltinTemplate(BuiltinFormatterFunction):
name = 'template'
arg_count = 1 category = RECURSION def __doc__getter__(self): return translate_ffml( r'''
``template(x)`` -- evaluates ``x`` as a template.[/] The evaluation is done in its own context, meaning that variables are not shared between the caller and the template evaluation. If not using General Program Mode, because the ``{`` and
``}`` characters are special, you must use ``[[`` for the ``{`` character and
``]]`` for the } character; they are converted automatically. For example,
``template(\'[[title_sort]]\')`` will evaluate the template ``{title_sort}`` and return its value. Note also that prefixes and suffixes (the ``|prefix|suffix`` syntax)
cannot be used in the argument to this function when using template program mode.
''')

def evaluate(self, formatter, kwargs, mi, locals, template):
template = template.replace('[[', '{').replace(']]', '}')
return formatter.__class__().safe_format(template, kwargs, 'TEMPLATE', mi)

[문서]
class BuiltinEval(BuiltinFormatterFunction):
name = 'eval'
arg_count = 1 category = RECURSION def __doc__getter__(self): return translate_ffml( r'''
``eval(string)`` -- evaluates the string as a program, passing the local variables.[/] This permits using the template processor to construct complex results from local variables. In
[URL href="https://manual.calibre-ebook.com/template_lang.html#more-complex-programs-in-template-expressions-template-program-mode"]
Template Program Mode[/URL], because the ``{`` and ``}`` characters are interpreted before the template is evaluated you must use ``[[`` for the ``{`` character and ``]]`` for the ``}`` character. They are converted automatically. Note also that prefixes and suffixes (the ``|prefix|suffix`` syntax) cannot be used in the argument to this function when using Template Program Mode.
''')

def evaluate(self, formatter, kwargs, mi, locals, template):
from calibre.utils.formatter import EvalFormatter template = template.replace('[[', '{').replace(']]', '}')
return EvalFormatter().safe_format(template, locals, 'EVAL', None)

[문서]
class BuiltinAssign(BuiltinFormatterFunction):
name = 'assign'
arg_count = 2 category = OTHER def __doc__getter__(self): return translate_ffml( r'''
``assign(id, value)`` -- assigns ``value`` to ``id``[/], then returns ``value``. ``id`` must be an identifier, not an expression. In most cases you can use the ``=`` operator instead of this function.
''')

def evaluate(self, formatter, kwargs, mi, locals, target, value):
locals[target] = value return value

[문서]
class BuiltinListSplit(BuiltinFormatterFunction):
name = 'list_split'
arg_count = 3 category = LIST_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``list_split(list_val, sep, id_prefix)`` -- splits ``list_val`` into separate values using ``sep``[/], then assigns the values to local variables named
``id_prefix_N`` where N is the position of the value in the list. The first item has position 0 (zero). The function returns the last element in the list.

Example:
[CODE]
list_split('one:two:foo', ':', 'var')
[/CODE]
is equivalent to:
[CODE]
var_0 = 'one'
var_1 = 'two'
var_2 = 'foo'
[/CODE]
''')

def evaluate(self, formatter, kwargs, mi, locals, list_val, sep, id_prefix):
l = [v.strip() for v in list_val.split(sep)]
res = ''
for i,v in enumerate(l):
res = locals[id_prefix+'_'+str(i)] = v return res

[문서]
class BuiltinPrint(BuiltinFormatterFunction):
name = 'print'
arg_count = -1 category = OTHER def __doc__getter__(self): return translate_ffml( r'''
``print(a [, b]*)`` -- prints the arguments to standard output.[/] Unless you start calibre from the command line (``calibre-debug -g``), the output will go into a black hole. The ``print`` function always returns its first argument.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
print(args)
return ''

[문서]
class BuiltinField(BuiltinFormatterFunction):
name = 'field'
arg_count = 1 category = GET_FROM_METADATA def __doc__getter__(self): return translate_ffml( r'''
``field(lookup_name)`` -- returns the value of the metadata field with lookup name ``lookup_name``.[/]
The ``$`` prefix can be used instead of the function, as in ``$tags``.
''')

def evaluate(self, formatter, kwargs, mi, locals, name):
return formatter.get_value(name, [], kwargs)

[문서]
class BuiltinRawField(BuiltinFormatterFunction):
name = 'raw_field'
arg_count = -1 category = GET_FROM_METADATA def __doc__getter__(self): return translate_ffml( r'''
``raw_field(lookup_name [, optional_default])`` -- returns the metadata field named by ``lookup_name`` without applying any formatting.[/] It evaluates and returns the optional second argument ``optional_default`` if the field's value is undefined (``None``). The ``$$`` prefix can be used instead of the function, as in ``$$pubdate``.
''')

def evaluate(self, formatter, kwargs, mi, locals, name, default=None):
res = getattr(mi, name, None)
if res is None and default is not None:
return default if isinstance(res, list):
fm = mi.metadata_for_field(name)
if fm is None:
return ', '.join(res)
return fm['is_multiple']['list_to_ui'].join(res)
return str(res)

[문서]
class BuiltinRawList(BuiltinFormatterFunction):
name = 'raw_list'
arg_count = 2 category = GET_FROM_METADATA def __doc__getter__(self): return translate_ffml( r'''
``raw_list(lookup_name, separator)`` -- returns the metadata list named by
``lookup_name`` without applying any formatting or sorting[/], with the items separated by ``separator``.
''')

def evaluate(self, formatter, kwargs, mi, locals, name, separator):
res = getattr(mi, name, None)
if not isinstance(res, list):
return f'{name} is not a list'
return separator.join(res)

[문서]
class BuiltinSubstr(BuiltinFormatterFunction):
name = 'substr'
arg_count = 3 category = STRING_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``substr(value, start, end)`` -- returns the ``start``'th through the ``end``'th characters of ``value``.[/] The first character in ``value`` is the zero'th character.
If ``end`` is negative then it indicates that many characters counting from the right. If ``end`` is zero, then it indicates the last character. For example,
``substr('12345', 1, 0)`` returns ``'2345'``, and ``substr('12345', 1, -1)`` returns ``'234'``.
''')

def evaluate(self, formatter, kwargs, mi, locals, value, start_, end_):
return value[int(start_): len(value) if int(end_) == 0 else int(end_)]

[문서]
class BuiltinLookup(BuiltinFormatterFunction):
name = 'lookup'
arg_count = -1 category = ITERATING_VALUES def __doc__getter__(self): return translate_ffml( r'''
``lookup(value, [ pattern, key, ]* else_key)`` -- The patterns will be checked against the ``value`` in order.[/] If a ``pattern`` matches then the value of the field named by
``key`` is returned. If no pattern matches then the value of the field named by
``else_key`` is returned. See also the :ref:`switch` function.
''')

def evaluate(self, formatter, kwargs, mi, locals, val, *args):
if len(args) == 2: # here for backwards compatibility if val:
return formatter.vformat('{'+args[0].strip()+'}', [], kwargs)
else:
return formatter.vformat('{'+args[1].strip()+'}', [], kwargs)
if (len(args) % 2) != 1:
raise ValueError(_('lookup requires either 2 or an odd number of arguments'))
i = 0 while i < len(args):
if i + 1 >= len(args):
return formatter.vformat('{' + args[i].strip() + '}', [], kwargs)
if re.search(args[i], val, flags=re.I):
return formatter.vformat('{'+args[i+1].strip() + '}', [], kwargs)
i += 2

[문서]
class BuiltinTest(BuiltinFormatterFunction):
name = 'test'
arg_count = 3 category = STRING_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``test(value, text_if_not_empty, text_if_empty)`` -- return ``text_if_not_empty`` if the value is not empty, otherwise return ``text_if_empty``.
''')

def evaluate(self, formatter, kwargs, mi, locals, val, value_if_set, value_not_set):
if val:
return value_if_set else:
return value_not_set

[문서]
class BuiltinContains(BuiltinFormatterFunction):
name = 'contains'
arg_count = 4 category = STRING_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``contains(value, pattern, text_if_match, text_if_not_match)`` -- checks if the value is matched by the regular expression ``pattern``.[/] Returns ``text_if_match`` if the pattern matches the value, otherwise returns ``text_if_not_match``.
''')

def evaluate(self, formatter, kwargs, mi, locals, val, test, value_if_present, value_if_not):
if re.search(test, val, flags=re.I):
return value_if_present else:
return value_if_not

[문서]
class BuiltinSwitch(BuiltinFormatterFunction):
name = 'switch'
arg_count = -1 category = ITERATING_VALUES def __doc__getter__(self): return translate_ffml( r'''
``switch(value, [patternN, valueN,]+ else_value)`` -- for each ``patternN, valueN`` pair, checks if the ``value`` matches the regular expression ``patternN``[/] and if so returns the associated ``valueN``. If no ``patternN`` matches, then ``else_value`` is returned. You can have as many ``patternN, valueN`` pairs as you wish. The first match is returned.
''')

def evaluate(self, formatter, kwargs, mi, locals, val, *args):
if (len(args) % 2) != 1:
raise ValueError(_('switch requires an even number of arguments'))
i = 0 while i < len(args):
if i + 1 >= len(args):
return args[i]
if re.search(args[i], val, flags=re.I):
return args[i+1]
i += 2

[문서]
class BuiltinSwitchIf(BuiltinFormatterFunction):
name = 'switch_if'
arg_count = -1 category = ITERATING_VALUES def __doc__getter__(self): return translate_ffml( r'''
``switch_if([test_expression, value_expression,]+ else_expression)`` -- for each
``test_expression, value_expression`` pair, checks if ``test_expression`` is
True (non-empty) and if so returns the result of ``value_expression``.[/] If no
``test_expression`` is True then the result of ``else_expression`` is returned.
You can have as many ``test_expression, value_expression`` pairs as you want.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
if (len(args) % 2) != 1:
raise ValueError(_('switch_if requires an odd number of arguments'))
# We shouldn't get here because the function is inlined. However, someone
# might call it directly.
i = 0 while i < len(args):
if i + 1 >= len(args):
return args[i]
if args[i]:
return args[i+1]
i += 2

[문서]
class BuiltinStrcatMax(BuiltinFormatterFunction):
name = 'strcat_max'
arg_count = -1 category = STRING_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``strcat_max(max, string1 [, prefix2, string2]*)`` -- Returns a string formed by concatenating the arguments.[/] The returned value is initialized to ``string1``.
Strings made from ``prefix, string`` pairs are added to the end of the value as long as the resulting string length is less than ``max``. Prefixes can be empty.
Returns ``string1`` even if ``string1`` is longer than ``max``. You can pass as many ``prefix, string`` pairs as you wish.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
if len(args) < 2:
raise ValueError(_('strcat_max requires 2 or more arguments'))
if (len(args) % 2) != 0:
raise ValueError(_('strcat_max requires an even number of arguments'))
try:
max = int(args[0])
except Exception:
raise ValueError(_('first argument to strcat_max must be an integer'))

i = 2 result = args[1]
try:
while i < len(args):
if (len(result) + len(args[i]) + len(args[i+1])) > max:
break result = result + args[i] + args[i+1]
i += 2 except Exception:
pass return result.strip()

[문서]
class BuiltinInList(BuiltinFormatterFunction):
name = 'list_contains'
arg_count = -1 category = LIST_LOOKUP def __doc__getter__(self): return translate_ffml( r'''
``list_contains(value, separator, [ pattern, found_val, ]* not_found_val)`` -- interpret the
``value`` as a list of items separated by ``separator``, checking the ``pattern`` against each item in the list.[/] If the ``pattern`` matches an item then return
``found_val``, otherwise return ``not_found_val``. The pair ``pattern`` and
``found_value`` can be repeated as many times as desired, permitting returning different values depending on the item's value. The patterns are checked in order, and the first match is returned.

Aliases: in_list(), list_contains()
''')

aliases = ['in_list']

def evaluate(self, formatter, kwargs, mi, locals, val, sep, *args):
if (len(args) % 2) != 1:
raise ValueError(_('in_list requires an odd number of arguments'))
l = [v.strip() for v in val.split(sep) if v.strip()]
i = 0 while i < len(args):
if i + 1 >= len(args):
return args[i]
sf = args[i]
fv = args[i+1]
if l:
for v in l:
if re.search(sf, v, flags=re.I):
return fv i += 2

[문서]
class BuiltinStrInList(BuiltinFormatterFunction):
name = 'str_in_list'
arg_count = -1 category = LIST_LOOKUP def __doc__getter__(self): return translate_ffml( r'''
``str_in_list(value, separator, [ string, found_val, ]+ not_found_val)`` -- interpret the ``value`` as a list of items separated by ``separator`` then compare ``string`` against each value in the list.[/] The ``string`` is not a regular expression. If
``string`` is equal to any item (ignoring case) then return the corresponding
``found_val``. If ``string`` contains ``separators`` then it is also treated as a list and each subvalue is checked. The ``string`` and ``found_value`` pairs can be repeated as many times as desired, permitting returning different values depending on string's value. If none of the strings match then
``not_found_value`` is returned. The strings are checked in order. The first match is returned.
''')

def evaluate(self, formatter, kwargs, mi, locals, val, sep, *args):
if (len(args) % 2) != 1:
raise ValueError(_('str_in_list requires an odd number of arguments'))
l = [v.strip() for v in val.split(sep) if v.strip()]
i = 0 while i < len(args):
if i + 1 >= len(args):
return args[i]
sf = args[i]
fv = args[i+1]
c = [v.strip() for v in sf.split(sep) if v.strip()]
if l:
for v in l:
for t in c:
if strcmp(t, v) == 0:
return fv i += 2[문서]
class BuiltinIdentifierInList(BuiltinFormatterFunction):
name = 'identifier_in_list'
arg_count = -1 category = LIST_LOOKUP def __doc__getter__(self): return translate_ffml( r'''
``identifier_in_list(val, id_name [, found_val, not_found_val])`` -- treat
``val`` as a list of identifiers separated by commas. An identifier has the format ``id_name:value``.[/] The ``id_name`` parameter is the id_name text to search for, either ``id_name`` or ``id_name:regexp``. The first case matches if there is any identifier matching that id_name. The second case matches if id_name matches an identifier and the regexp matches the identifier's value. If
``found_val`` and ``not_found_val`` are provided then if there is a match then return ``found_val``, otherwise return ``not_found_val``. If ``found_val`` and
``not_found_val`` are not provided then if there is a match then return the
``identifier:value`` pair, otherwise the empty string (``''``).
''')

def evaluate(self, formatter, kwargs, mi, locals, val, ident, *args):
if len(args) == 0:
fv_is_id = True nfv = ''
elif len(args) == 2:
fv_is_id = False fv = args[0]
nfv = args[1]
else:
raise ValueError(_('{} requires 2 or 4 arguments').format(self.name))

l = [v.strip() for v in val.split(',') if v.strip()]
id_, __, regexp = ident.partition(':')
if not id_:
return nfv for candidate in l:
i, __, v = candidate.partition(':')
if v and i == id_:
if not regexp or re.search(regexp, v, flags=re.I):
return candidate if fv_is_id else fv return nfv

[문서]
class BuiltinRe(BuiltinFormatterFunction):
name = 're'
arg_count = 3 category = STRING_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``re(value, pattern, replacement)`` -- return the ``value`` after applying the regular expression.[/] All instances of ``pattern`` in the value are replaced with
``replacement``. The template language uses case insensitive
[URL href="https://docs.python.org/3/library/re.html"]Python regular expressions[/URL].
''')

def evaluate(self, formatter, kwargs, mi, locals, val, pattern, replacement):
return re.sub(pattern, replacement, val, flags=re.I)

[문서]
class BuiltinReGroup(BuiltinFormatterFunction):
name = 're_group'
arg_count = -1 category = STRING_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``re_group(value, pattern [, template_for_group]*)`` -- return a string made by applying the regular expression ``pattern`` to ``value`` and replacing each matched instance[/] with the value returned by the corresponding template. In
[URL href="https://manual.calibre-ebook.com/template_lang.html#more-complex-programs-in-template-expressions-template-program-mode"]
Template Program Mode[/URL], like for the ``template`` and the
``eval`` functions, you use ``[[`` for ``{`` and ``]]`` for ``}``.

The following example looks for a series with more than one word and uppercases the first word:
[CODE]
program: re_group(field('series'), "(\S* )(.*)", "{$:uppercase()}", "{$}")'}
[/CODE]
''')

def evaluate(self, formatter, kwargs, mi, locals, val, pattern, *args):
from calibre.utils.formatter import EvalFormatter def repl(mo):
res = ''
if mo and mo.lastindex:
for dex in range(mo.lastindex):
gv = mo.group(dex+1)
if gv is None:
continue if len(args) > dex:
template = args[dex].replace('[[', '{').replace(']]', '}')
res += EvalFormatter().safe_format(template, {'$': gv},
'EVAL', None, strip_results=False)
else:
res += gv return res return re.sub(pattern, repl, val, flags=re.I)

[문서]
class BuiltinSwapAroundComma(BuiltinFormatterFunction):
name = 'swap_around_comma'
arg_count = 1 category = STRING_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``swap_around_comma(value)`` -- given a ``value`` of the form ``B, A``, return ``A B``.[/]
This is most useful for converting names in LN, FN format to FN LN. If there is no comma in the ``value`` then the function returns the value unchanged.
''')

def evaluate(self, formatter, kwargs, mi, locals, val):
return re.sub(r'^(.*?),\s*(.*$)', r'\2 \1', val, flags=re.I).strip()

[문서]
class BuiltinIfempty(BuiltinFormatterFunction):
name = 'ifempty'
arg_count = 2 category = STRING_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``ifempty(value, text_if_empty)`` -- if the ``value`` is not empty then return that ``value``, otherwise return ``text_if_empty``.
''')

def evaluate(self, formatter, kwargs, mi, locals, val, value_if_empty):
if val:
return val else:
return value_if_empty

[문서]
class BuiltinShorten(BuiltinFormatterFunction):
name = 'shorten'
arg_count = 4 category = STRING_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``shorten(value, left_chars, middle_text, right_chars)`` -- Return a shortened version of the ``value``[/], consisting of ``left_chars`` characters from the beginning of the
``value``, followed by ``middle_text``, followed by ``right_chars`` characters from the end of the ``value``. ``left_chars`` and ``right_chars`` must be non-negative integers.

Example: assume you want to display the title with a length of at most
15 characters in length. One template that does this is
``{title:shorten(9,-,5)}``. For a book with the title `Ancient English Laws in the Times of Ivanhoe` the result will be `Ancient E-anhoe`: the first 9 characters of the title, a ``-``, then the last 5 characters. If the value's length is less than ``left chars`` + ``right chars`` + the length of ``middle text`` then the value will be returned unchanged. For example, the title `The
Dome` would not be changed.
''')

def evaluate(self, formatter, kwargs, mi, locals, val, leading, center_string, trailing):
l = max(0, int(leading))
t = max(0, int(trailing))
if len(val) > l + len(center_string) + t:
return val[0:l] + center_string + ('' if t == 0 else val[-t:])
else:
return val

[문서]
class BuiltinCount(BuiltinFormatterFunction):
name = 'list_count'
arg_count = 2 category = LIST_MANIPULATION aliases = ['count']

def __doc__getter__(self): return translate_ffml( r'''
``list_count(value, separator)`` -- interprets the value as a list of items separated by
``separator`` and returns the number of items in the list.[/] Most lists use a comma as the separator, but ``authors`` uses an ampersand (&).

Examples: ``{tags:list_count(,)}``, ``{authors:list_count(&)}``.

Aliases: ``count()``, ``list_count()``
''')

def evaluate(self, formatter, kwargs, mi, locals, val, sep):
return str(len([v for v in val.split(sep) if v]))

[문서]
class BuiltinListCountMatching(BuiltinFormatterFunction):
name = 'list_count_matching'
arg_count = 3 category = LIST_MANIPULATION aliases = ['count_matching']

def __doc__getter__(self): return translate_ffml( r'''
``list_count_matching(value, pattern, separator)`` -- interprets ``value`` as a list of items separated by ``separator``, returning the number of items in the list that match the regular expression ``pattern``.[/]

Aliases: ``list_count_matching()``, ``count_matching()``
''')

def evaluate(self, formatter, kwargs, mi, locals, value, pattern, sep):
res = 0 for v in [x.strip() for x in value.split(sep) if x.strip()]:
if re.search(pattern, v, flags=re.I):
res += 1 return str(res)

[문서]
class BuiltinListitem(BuiltinFormatterFunction):
name = 'list_item'
arg_count = 3 category = LIST_LOOKUP def __doc__getter__(self): return translate_ffml( r'''
``list_item(value, index, separator)`` -- interpret the ``value`` as a list of items separated by ``separator``, returning the 'index'th item.[/] The first item is number zero. The last item has the index ``-1`` as in
``list_item(-1,separator)``. If the item is not in the list, then the empty string is returned. The separator has the same meaning as in the count function, usually comma but is ampersand for author-like lists.
''')

def evaluate(self, formatter, kwargs, mi, locals, val, index, sep):
if not val:
return ''
index = int(index)
val = val.split(sep)
try:
return val[index].strip()
except Exception:
return ''

[문서]
class BuiltinSelect(BuiltinFormatterFunction):
name = 'select'
arg_count = 2 category = LIST_LOOKUP def __doc__getter__(self): return translate_ffml( r'''
``select(value, key)`` -- interpret the ``value`` as a comma-separated list of items with each item having the form ``id:id_value`` (the calibre ``identifier`` format).[/] The function finds the first pair with the id equal to ``key`` and returns the corresponding ``id_value``. If no id matches then the function returns the empty string.
''')

def evaluate(self, formatter, kwargs, mi, locals, val, key):
if not val:
return ''
vals = [v.strip() for v in val.split(',')]
tkey = key+':'
for v in vals:
if v.startswith(tkey):
return v[len(tkey):]
return ''

[문서]
class BuiltinApproximateFormats(BuiltinFormatterFunction):
name = 'approximate_formats'
arg_count = 0 category = DB_FUNCS def __doc__getter__(self): return translate_ffml( r'''
``approximate_formats()`` -- return a comma-separated list of formats associated with the book.[/] Because the list comes from calibre's database instead of the file system, there is no guarantee that the list is correct, although it probably is. Note that resulting format names are always uppercase, as in EPUB.
The ``approximate_formats()`` function is much faster than the ``formats_...`` functions.

This function works only in the GUI. If you want to use these values in save-to-disk or send-to-device templates then you must make a custom "Column built from other columns", use the function in that column's template, and use that column's value in your save/send templates.
''')

def evaluate(self, formatter, kwargs, mi, locals):
if hasattr(mi, '_proxy_metadata'):
fmt_data = mi._proxy_metadata.db_approx_formats if not fmt_data:
return ''
data = sorted(fmt_data)
return ','.join(v.upper() for v in data)
self.only_in_gui_error()

[문서]
class BuiltinFormatsModtimes(BuiltinFormatterFunction):
name = 'formats_modtimes'
arg_count = 1 category = DB_FUNCS def __doc__getter__(self): return translate_ffml( r'''
``formats_modtimes(date_format_string)`` -- return a comma-separated list of colon-separated items ``FMT:DATE`` representing modification times for the formats of a book.[/] The ``date_format_string`` parameter specifies how the date is to be formatted. See the :ref:`format_date` function for details. You can use the :ref:`select` function to get the modification time for a specific format. Note that format names are always uppercase, as in EPUB.
''')

def evaluate(self, formatter, kwargs, mi, locals, fmt):
fmt_data = mi.get('format_metadata', {})
try:
data = sorted(fmt_data.items(), key=lambda x:x[1]['mtime'], reverse=True)
return ','.join(k.upper()+':'+format_date(v['mtime'], fmt)
for k,v in data)
except Exception:
return ''

[문서]
class BuiltinFormatsSizes(BuiltinFormatterFunction):
name = 'formats_sizes'
arg_count = 0 category = DB_FUNCS def __doc__getter__(self): return translate_ffml( r'''

``formats_sizes()`` -- return a comma-separated list of colon-separated
``FMT:SIZE`` items giving the sizes of the formats of a book in bytes.[/] You can use the ``select()`` function to get the size for a specific format. Note that format names are always uppercase, as in EPUB.
''')

def evaluate(self, formatter, kwargs, mi, locals):
fmt_data = mi.get('format_metadata', {})
try:
return ','.join(k.upper()+':'+str(v['size']) for k,v in fmt_data.items())
except Exception:
return ''

[문서]
class BuiltinFormatsPaths(BuiltinFormatterFunction):
name = 'formats_paths'
arg_count = -1 category = DB_FUNCS def __doc__getter__(self): return translate_ffml( r'''
``formats_paths([separator])`` -- return a ``separator``-separated list of colon-separated items ``FMT:PATH`` giving the full path to the formats of a book.[/] The ``separator`` argument is optional. If not supplied then the separator is ``', '`` (comma space). If the separator is a comma then you can use the ``select()`` function to get the path for a specific format. Note that format names are always uppercase, as in EPUB.
''')

def evaluate(self, formatter, kwargs, mi, locals, sep=','):
fmt_data = mi.get('format_metadata', {})
try:
return sep.join(k.upper()+':'+str(v['path']) for k,v in fmt_data.items())
except Exception:
return ''

[문서]
class BuiltinFormatsPathSegments(BuiltinFormatterFunction):
name = 'formats_path_segments'
arg_count = 5 category = DB_FUNCS def __doc__getter__(self): return translate_ffml( r'''
``formats_path_segments(with_author, with_title, with_format, with_ext, sep)``
-- return parts of the path to a book format in the calibre library separated by ``sep``.[/] The parameter ``sep`` should usually be a slash (``'/'``). One use is to be sure that paths generated in Save to disk and Send to device templates are shortened consistently. Another is to be sure the paths on the device match the paths in the calibre library.

A book path consists of 3 segments: the author, the title including the calibre database id in parentheses, and the format (author - title). Calibre can shorten any of the three because of file name length limitations. You choose which segments to include by passing ``1`` for that segment. If you don't want a segment then pass ``0`` or the empty string for that segment. For example, the following returns just the format name without the extension:
[CODE]
formats_path_segments(0, 0, 1, 0, '/')
[/CODE]
Because there is only one segment the separator is ignored.

If there are multiple formats (multiple extensions) then one of the extensions will be picked at random. If you care about which extension is used then get the path without the extension then add the desired extension to it.

Examples: Assume there is a book in the calibre library with an epub format by
Joe Blogs with title 'Help'. It would have the path
[CODE]
Joe Blogs/Help - (calibre_id)/Help - Joe Blogs.epub
[/CODE]
The following shows what is returned for various parameters:
[LIST]
[*]``formats_path_segments(0, 0, 1, 0, '/')`` returns `Help - Joe Blogs`
[*]``formats_path_segments(0, 0, 1, 1, '/')`` returns `Help - Joe Blogs.epub`
[*]``formats_path_segments(1, 0, 1, 1, '/')`` returns `Joe Blogs/Help - Joe Blogs.epub`
[*]``formats_path_segments(1, 0, 1, 0, '/')`` returns `Joe Blogs/Help - Joe Blogs`
[*]``formats_path_segments(0, 1, 0, 0, '/')`` returns `Help - (calibre_id)`
[/LIST]
''')

def evaluate(self, formatter, kwargs, mi, locals, with_author, with_title, with_format, with_ext, sep):
fmt_metadata = mi.get('format_metadata', {})
if fmt_metadata:
for v in fmt_metadata.values():
p = v['path']
r,fmt = os.path.split(p)
if with_ext == '0' or not with_ext:
fmt = os.path.splitext(fmt)[0]
r,title = os.path.split(r)
r,author = os.path.split(r)
parts = []
if with_author == '1':
parts.append(author)
if with_title == '1':
parts.append(title)
if with_format == '1':
parts.append(fmt)
return sep.join(parts)
else:
return _("No book formats found so the path can't be generated")

[문서]
class BuiltinHumanReadable(BuiltinFormatterFunction):
name = 'human_readable'
arg_count = 1 category = FORMATTING_VALUES def __doc__getter__(self): return translate_ffml( r'''
``human_readable(value)`` -- expects the ``value`` to be a number and returns a string representing that number in KB, MB, GB, etc.
''')

def evaluate(self, formatter, kwargs, mi, locals, val):
try:
return human_readable(round(float(val)))
except Exception:
return ''

[문서]
class BuiltinFormatNumber(BuiltinFormatterFunction):
name = 'format_number'
arg_count = 2 category = FORMATTING_VALUES def __doc__getter__(self): return translate_ffml( r'''
``format_number(value, template)`` -- interprets the ``value`` as a number and formats that number using a Python formatting template such as ``{0:5.2f}`` or ``{0:,d}`` or
``${0:5,.2f}``.[/] The formatting template must begin with ``{0:`` and end with
``}`` as in the above examples. Exception: you can leave off the leading "{0:"
and trailing "}" if the format template contains only a format. See the
[URL href="https://manual.calibre-ebook.com/template_lang.html"]
Template Language[/URL] and the
[URL href="https://docs.python.org/3/library/string.html#formatstrings"]
Python[/URL] documentation for more examples. Returns the empty string if formatting fails.
''')

def evaluate(self, formatter, kwargs, mi, locals, val, template):
if val in {'', 'None'}:
return ''
if '{' not in template:
template = '{0:' + template + '}'
try:
v1 = float(val)
except Exception:
return ''
try: # Try formatting the value as a float return template.format(v1)
except Exception:
pass try: # Try formatting the value as an int v2 = trunc(v1)
if v2 == v1:
return template.format(v2)
except Exception:
pass return ''

[문서]
class BuiltinSublist(BuiltinFormatterFunction):
name = 'sublist'
arg_count = 4 category = LIST_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``sublist(value, start_index, end_index, separator)`` -- interpret the ``value`` as a list of items separated by ``separator``, returning a new list made from the items from ``start_index`` to ``end_index``.[/] The first item is number zero. If an index is negative, then it counts from the end of the list. As a special case, an end_index of zero is assumed to be the length of the list.

Examples assuming that the tags column (which is comma-separated) contains "A, B, C":
[LIST]
[*]``{tags:sublist(0,1,\,)}`` returns "A"
[*]``{tags:sublist(-1,0,\,)}`` returns "C"
[*]``{tags:sublist(0,-1,\,)}`` returns "A, B"
[/LIST]
''')

def evaluate(self, formatter, kwargs, mi, locals, val, start_index, end_index, sep):
if not val:
return ''
si = int(start_index)
ei = int(end_index)
# allow empty list items so counts are what the user expects val = [v.strip() for v in val.split(sep)]

if sep == ',':
sep = ', '
try:
if ei == 0:
return sep.join(val[si:])
else:
return sep.join(val[si:ei])
except Exception:
return ''

[문서]
class BuiltinSubitems(BuiltinFormatterFunction):
name = 'subitems'
arg_count = 3 category = LIST_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``subitems(value, start_index, end_index)`` -- This function breaks apart lists of tag-like hierarchical items such as genres.[/] It interprets the ``value`` as a comma- separated list of tag-like items, where each item is a period-separated list. It returns a new list made by extracting from each item the components from
``start_index`` to ``end_index``, then merging the results back together.
Duplicates are removed. The first subitem in a period-separated list has an index of zero. If an index is negative then it counts from the end of the list.
As a special case, an ``end_index`` of zero is assumed to be the length of the list.

Examples:
[LIST]
[*]Assuming a #genre column containing "A.B.C":
[LIST]
[*]``{#genre:subitems(0,1)}`` returns "A"
[*]``{#genre:subitems(0,2)}`` returns "A.B"
[*]``{#genre:subitems(1,0)}`` returns "B.C"
[/LIST]
[*]Assuming a #genre column containing "A.B.C, D.E":
[LIST]
[*]``{#genre:subitems(0,1)}`` returns "A, D"
[*]``{#genre:subitems(0,2)}`` returns "A.B, D.E"
[/LIST]
[/LIST]
''')

period_pattern = re.compile(r'(?<=[^\.\s])\.(?=[^\.\s])', re.U)

def evaluate(self, formatter, kwargs, mi, locals, val, start_index, end_index):
if not val:
return ''
si = int(start_index)
ei = int(end_index)
has_periods = '.' in val items = [v.strip() for v in val.split(',') if v.strip()]
rv = set()
for item in items:
if has_periods and '.' in item:
components = self.period_pattern.split(item)
else:
components = [item]
try:
if ei == 0:
t = '.'.join(components[si:]).strip()
else:
t = '.'.join(components[si:ei]).strip()
if t:
rv.add(t)
except Exception:
pass return ', '.join(sorted(rv, key=sort_key))[문서]
class BuiltinFormatDate(BuiltinFormatterFunction):
name = 'format_date'
arg_count = 2 category = FORMATTING_VALUES def __doc__getter__(self): return translate_ffml( r'''
``format_date(value, format_string)`` -- format the ``value``, which must be a date string, using the ``format_string``, returning a string.[/] It is best if the date is in ISO format as using other date formats often causes errors because the actual date value cannot be unambiguously determined. Note that the
``format_date_field()`` function is both faster and more reliable.

The formatting codes are:
[LIST]
[*]``d :`` the day as number without a leading zero (1 to 31)
[*]``dd :`` the day as number with a leading zero (01 to 31)
[*]``ddd :`` the abbreviated localized day name (e.g. "Mon" to "Sun")
[*]``dddd :`` the long localized day name (e.g. "Monday" to "Sunday")
[*]``M :`` the month as number without a leading zero (1 to 12)
[*]``MM :`` the month as number with a leading zero (01 to 12)
[*]``MMM :`` the abbreviated localized month name (e.g. "Jan" to "Dec")
[*]``MMMM :`` the long localized month name (e.g. "January" to "December")
[*]``yy :`` the year as two digit number (00 to 99)
[*]``yyyy :`` the year as four digit number.
[*]``h :`` the hours without a leading 0 (0 to 11 or 0 to 23, depending on am/pm)
[*]``hh :`` the hours with a leading 0 (00 to 11 or 00 to 23, depending on am/pm)
[*]``m :`` the minutes without a leading 0 (0 to 59)
[*]``mm :`` the minutes with a leading 0 (00 to 59)
[*]``s :`` the seconds without a leading 0 (0 to 59)
[*]``ss :`` the seconds with a leading 0 (00 to 59)
[*]``ap :`` use a 12-hour clock instead of a 24-hour clock, with 'ap' replaced by the lowercase localized string for am or pm
[*]``AP :`` use a 12-hour clock instead of a 24-hour clock, with 'AP' replaced by the uppercase localized string for AM or PM
[*]``aP :`` use a 12-hour clock instead of a 24-hour clock, with 'aP' replaced by the localized string for AM or PM
[*]``Ap :`` use a 12-hour clock instead of a 24-hour clock, with 'Ap' replaced by the localized string for AM or PM
[*]``iso :`` the date with time and timezone. Must be the only format present
[*]``to_number :`` convert the date & time into a floating point number (a `timestamp`)
[*]``from_number :`` convert a floating point number (a `timestamp`) into an
ISO-formatted date. If you want a different date format then add the desired formatting string after ``from_number`` and a colon (``:``). Example:
[CODE]
format_date(val, 'from_number:MMM dd yyyy')
[/CODE]
[/LIST]
You might get unexpected results if the date you are formatting contains localized month names, which can happen if you changed the date format to contain ``MMMM``. Using ``format_date_field()`` avoids this problem.
''')

def evaluate(self, formatter, kwargs, mi, locals, val, format_string):
if not val or val == 'None':
return ''
try:
if format_string == 'to_number':
s = parse_date(val).timestamp()
elif format_string.startswith('from_number'):
val = datetime.fromtimestamp(float(val))
f = format_string[12:]
s = format_date(val, f or 'iso')
else:
s = format_date(parse_date(val), format_string)
return s except Exception:
s = 'BAD DATE'
return s[문서]
class BuiltinFormatDateField(BuiltinFormatterFunction):
name = 'format_date_field'
arg_count = 2 category = FORMATTING_VALUES def __doc__getter__(self): return translate_ffml( r'''
``format_date_field(field_name, format_string)`` -- format the value in the field ``field_name``, which must be the lookup name of a date field, either standard or custom.[/] See :ref:`format_date` for the formatting codes. This function is much faster than format_date() and should be used when you are formatting the value in a field (column). It is also more reliable because it works directly on the underlying date. It can't be used for computed dates or dates in string variables. Examples:
[CODE]
format_date_field('pubdate', 'yyyy.MM.dd')
format_date_field('#date_read', 'MMM dd, yyyy')
[/CODE]
''')

def evaluate(self, formatter, kwargs, mi, locals, field, format_string):
try:
field = field_metadata.search_term_to_field_key(field)
if field not in mi.all_field_keys():
raise ValueError(_("Function {0}: Unknown field '{1}'").format('format_date_field', field))
val = mi.get(field, None)
if mi.metadata_for_field(field)['datatype'] != 'datetime':
raise ValueError(_("Function {0}: field '{1}' is not a date").format('format_date_field', field))
if val is None:
s = ''
elif format_string == 'to_number':
s = val.timestamp()
elif format_string.startswith('from_number'):
val = datetime.fromtimestamp(float(val))
f = format_string[12:]
s = format_date(val, f or 'iso')
else:
s = format_date(val, format_string)
return s except ValueError:
raise except Exception:
traceback.print_exc()
raise return s

[문서]
class BuiltinUppercase(BuiltinFormatterFunction):
name = 'uppercase'
arg_count = 1 category = CASE_CHANGES def __doc__getter__(self): return translate_ffml( r'''
``uppercase(value)`` -- returns the ``value`` in upper case.
''')

def evaluate(self, formatter, kwargs, mi, locals, val):
return val.upper()

[문서]
class BuiltinLowercase(BuiltinFormatterFunction):
name = 'lowercase'
arg_count = 1 category = CASE_CHANGES def __doc__getter__(self): return translate_ffml( r'''
``lowercase(value)`` -- returns the ``value`` in lower case.
''')

def evaluate(self, formatter, kwargs, mi, locals, val):
return val.lower()

[문서]
class BuiltinTitlecase(BuiltinFormatterFunction):
name = 'titlecase'
arg_count = 1 category = CASE_CHANGES def __doc__getter__(self): return translate_ffml( r'''
``titlecase(value)`` -- returns the ``value`` in title case.
''')

def evaluate(self, formatter, kwargs, mi, locals, val):
return titlecase(val)

[문서]
class BuiltinCapitalize(BuiltinFormatterFunction):
name = 'capitalize'
arg_count = 1 category = CASE_CHANGES def __doc__getter__(self): return translate_ffml( r'''
``capitalize(value)`` -- returns the ``value`` with the first letter in upper case and the rest lower case.
''')

def evaluate(self, formatter, kwargs, mi, locals, val):
return capitalize(val)

[문서]
class BuiltinBooksize(BuiltinFormatterFunction):
name = 'booksize'
arg_count = 0 category = GET_FROM_METADATA def __doc__getter__(self): return translate_ffml( r'''
``booksize()`` -- returns the value of the calibre ``size`` field. Returns '' if the book has no formats.[/]

This function works only in the GUI. If you want to use this value in save-to-disk or send-to-device templates then you must make a custom "Column built from other columns", use the function in that column's template, and use that column's value in your save/send templates
''')

def evaluate(self, formatter, kwargs, mi, locals):
if hasattr(mi, '_proxy_metadata'):
try:
v = mi._proxy_metadata.book_size if v is not None:
return str(mi._proxy_metadata.book_size)
return ''
except Exception:
pass return ''
self.only_in_gui_error()

[문서]
class BuiltinOndevice(BuiltinFormatterFunction):
name = 'ondevice'
arg_count = 0 category = GET_FROM_METADATA def __doc__getter__(self): return translate_ffml( r'''
``ondevice()`` -- return the string ``'Yes'`` if ``ondevice`` is set, otherwise return the empty string.[/] This function works only in the GUI. If you want to use this value in save-to-disk or send-to-device templates then you must make a custom "Column built from other columns", use the function in that column\'s template, and use that column\'s value in your save/send templates.
''')

def evaluate(self, formatter, kwargs, mi, locals):
if hasattr(mi, '_proxy_metadata'):
if mi._proxy_metadata.ondevice_col:
return _('Yes')
return ''
self.only_in_gui_error()

[문서]
class BuiltinAnnotationCount(BuiltinFormatterFunction):
name = 'annotation_count'
arg_count = 0 category = DB_FUNCS def __doc__getter__(self): return translate_ffml( r'''
``annotation_count()`` -- return the total number of annotations of all types attached to the current book.[/] This function works only in the GUI and the content server.
''')

def evaluate(self, formatter, kwargs, mi, locals):
c = self.get_database(mi, formatter=formatter).new_api.annotation_count_for_book(mi.id)
return '' if c == 0 else str(c)

[문서]
class BuiltinIsMarked(BuiltinFormatterFunction):
name = 'is_marked'
arg_count = 0 category = GET_FROM_METADATA def __doc__getter__(self): return translate_ffml( r'''
``is_marked()`` -- check whether the book is `marked` in calibre.[/] If it is then return the value of the mark, either ``'true'`` (lower case) or a comma-separated list of named marks. Returns ``''`` (the empty string) if the book is not marked. This function works only in the GUI.
''')

def evaluate(self, formatter, kwargs, mi, locals):
c = self.get_database(mi, formatter=formatter).data.get_marked(mi.id)
return c or ''

[문서]
class BuiltinSeriesSort(BuiltinFormatterFunction):
name = 'series_sort'
arg_count = 0 category = GET_FROM_METADATA def __doc__getter__(self): return translate_ffml( r'''
``series_sort()`` -- returns the series sort value.
''')

def evaluate(self, formatter, kwargs, mi, locals):
if mi.series:
langs = mi.languages lang = langs[0] if langs else None return title_sort(mi.series, lang=lang)
return ''

[문서]
class BuiltinHasCover(BuiltinFormatterFunction):
name = 'has_cover'
arg_count = 0 category = GET_FROM_METADATA def __doc__getter__(self): return translate_ffml( r'''
``has_cover()`` -- return ``'Yes'`` if the book has a cover, otherwise the empty string.
''')

def evaluate(self, formatter, kwargs, mi, locals):
if mi.has_cover:
return _('Yes')
return ''

[문서]
class BuiltinFirstNonEmpty(BuiltinFormatterFunction):
name = 'first_non_empty'
arg_count = -1 category = ITERATING_VALUES def __doc__getter__(self): return translate_ffml( r'''
``first_non_empty(value [, value]*)`` -- returns the first ``value`` that is not empty.[/] If all values are empty, then the empty string is returned. You can have as many values as you want.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
i = 0 while i < len(args):
if args[i]:
return args[i]
i += 1 return ''

[문서]
class BuiltinAnd(BuiltinFormatterFunction):
name = 'and'
arg_count = -1 category = BOOLEAN def __doc__getter__(self): return translate_ffml( r'''
``and(value [, value]*)`` -- returns the string ``'1'`` if all values are not empty, otherwise returns the empty string.[/] You can have as many values as you want. In most cases you can use the ``&&`` operator instead of this function. One reason not to replace ``and()`` with ``&&`` is when short-circuiting can change the results because of side effects. For example, ``and(a='',b=5)`` will always do both assignments, where the ``&&`` operator won't do the second.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
i = 0 while i < len(args):
if not args[i]:
return ''
i += 1 return '1'

[문서]
class BuiltinOr(BuiltinFormatterFunction):
name = 'or'
arg_count = -1 category = BOOLEAN def __doc__getter__(self): return translate_ffml( r'''
``or(value [, value]*)`` -- returns the string ``'1'`` if any value is not empty, otherwise returns the empty string.[/] You can have as many values as you want. This function can usually be replaced by the ``||`` operator. A reason it cannot be replaced is if short-circuiting will change the results because of side effects.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
i = 0 while i < len(args):
if args[i]:
return '1'
i += 1 return ''

[문서]
class BuiltinNot(BuiltinFormatterFunction):
name = 'not'
arg_count = 1 category = BOOLEAN def __doc__getter__(self): return translate_ffml( r'''
``not(value)`` -- returns the string ``'1'`` if the value is empty, otherwise returns the empty string.[/] This function can usually be replaced with the unary not (``!``) operator.
''')

def evaluate(self, formatter, kwargs, mi, locals, val):
return '' if val else '1'

[문서]
class BuiltinListJoin(BuiltinFormatterFunction):
name = 'list_join'
arg_count = -1 category = LIST_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``list_join(with_separator, list1, separator1 [, list2, separator2]*)`` -- return a list made by joining the items in the source lists[/] (``list1`` etc)
using ``with_separator`` between the items in the result list. Items in each source ``list[123...]`` are separated by the associated ``separator[123...]``. A list can contain zero values. It can be a field like ``publisher`` that is single-valued, effectively a one-item list. Duplicates are removed using a case-insensitive comparison. Items are returned in the order they appear in the source lists. If items on lists differ only in letter case then the last is used. All separators can be more than one character.

Example:
[CODE]
program:
list_join('#@#', $authors, '&', $tags, ',')
[/CODE]
You can use ``list_join`` on the results of previous calls to ``list_join`` as follows:
[CODE]
program:
a = list_join('#@#', $authors, '&', $tags, ','); b = list_join('#@#', a, '#@#', $#genre, ',', $#people, '&', 'some value', ',')
[/CODE]
You can use expressions to generate a list. For example, assume you want items for ``authors`` and ``#genre``, but with the genre changed to the word "Genre: "
followed by the first letter of the genre, i.e. the genre "Fiction" becomes
"Genre: F". The following will do that:
{}''').format('''\
[CODE]
program:
list_join('#@#', $authors, '&', list_re($#genre, ',', '^(.).*$', 'Genre: \\1'), ',')
[/CODE]
''') # not translated as \1 gets mistranslated as a control char in transifex
# for some reason. And yes, the double backslash is required, for some reason.

def evaluate(self, formatter, kwargs, mi, locals, with_separator, *args):
if len(args) % 2 != 0:
raise ValueError(
_("Invalid 'List, separator' pairs. Every list must have one "
"associated separator"))

# Starting in python 3.7 dicts preserve order so we don't need OrderedDict result = {}
i = 0 while i < len(args):
lst = [v.strip() for v in args[i].split(args[i+1]) if v.strip()]
result.update({item.lower():item for item in lst})
i += 2 return with_separator.join(result.values())

[문서]
class BuiltinListUnion(BuiltinFormatterFunction):
name = 'list_union'
arg_count = 3 category = LIST_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``list_union(list1, list2, separator)`` -- return a list made by merging the items in ``list1`` and ``list2``[/], removing duplicate items using a case-insensitive comparison. If items differ in case, the one in ``list1`` is used.
The items in ``list1`` and ``list2`` are separated by ``separator``, as are the items in the returned list.

Aliases: ``merge_lists()``, ``list_union()``
''')
aliases = ['merge_lists']

def evaluate(self, formatter, kwargs, mi, locals, list1, list2, separator):
res = {icu_lower(l.strip()): l.strip() for l in list2.split(separator) if l.strip()}
res.update({icu_lower(l.strip()): l.strip() for l in list1.split(separator) if l.strip()})
if separator == ',':
separator = ', '
return separator.join(res.values())

[문서]
class BuiltinRange(BuiltinFormatterFunction):
name = 'range'
arg_count = -1 category = LIST_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``range(start, stop, step, limit)`` -- returns a list of numbers generated by looping over the range specified by the parameters ``start``, ``stop``, and ``step``, with a maximum length of ``limit``.[/] The first value produced is 'start'. Subsequent values
``next_v = current_v + step``. The loop continues while ``next_v < stop`` assuming ``step`` is positive, otherwise while ``next_v > stop``. An empty list is produced if ``start`` fails the test: ``start >= stop`` if ``step`` is positive. The ``limit`` sets the maximum length of the list and has a default of
1000. The parameters ``start``, ``step``, and ``limit`` are optional. Calling
``range()`` with one argument specifies ``stop``. Two arguments specify
``start`` and ``stop``. Three arguments specify ``start``, ``stop``, and
``step``. Four arguments specify ``start``, ``stop``, ``step`` and ``limit``.

Examples:
[CODE]
range(5) -> '0, 1, 2, 3, 4'
range(0, 5) -> '0, 1, 2, 3, 4'
range(-1, 5) -> '-1, 0, 1, 2, 3, 4'
range(1, 5) -> '1, 2, 3, 4'
range(1, 5, 2) -> '1, 3'
range(1, 5, 2, 5) -> '1, 3'
range(1, 5, 2, 1) -> error(limit exceeded)
[/CODE]
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
limit_val = 1000 start_val = 0 step_val = 1 if len(args) == 1:
stop_val = int(args[0] if args[0] and args[0] != 'None' else 0)
elif len(args) == 2:
start_val = int(args[0] if args[0] and args[0] != 'None' else 0)
stop_val = int(args[1] if args[1] and args[1] != 'None' else 0)
elif len(args) >= 3:
start_val = int(args[0] if args[0] and args[0] != 'None' else 0)
stop_val = int(args[1] if args[1] and args[1] != 'None' else 0)
step_val = int(args[2] if args[2] and args[2] != 'None' else 0)
if len(args) > 3:
limit_val = int(args[3] if args[3] and args[3] != 'None' else 0)
r = range(start_val, stop_val, step_val)
if len(r) > limit_val:
raise ValueError(
_('{0}: length ({1}) longer than limit ({2})').format(
'range', len(r), str(limit_val)))
return ', '.join([str(v) for v in r])

[문서]
class BuiltinListRemoveDuplicates(BuiltinFormatterFunction):
name = 'list_remove_duplicates'
arg_count = 2 category = LIST_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``list_remove_duplicates(list, separator)`` -- return a list made by removing duplicate items in ``list``.[/] If items differ only in case then the last is returned. The items in ``list`` are separated by ``separator``, as are the items in the returned list.
''')

def evaluate(self, formatter, kwargs, mi, locals, list_, separator):
res = {icu_lower(l.strip()): l.strip() for l in list_.split(separator) if l.strip()}
if separator == ',':
separator = ', '
return separator.join(res.values())

[문서]
class BuiltinListDifference(BuiltinFormatterFunction):
name = 'list_difference'
arg_count = 3 category = LIST_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``list_difference(list1, list2, separator)`` -- return a list made by removing from ``list1`` any item found in ``list2``[/] using a case-insensitive comparison.
The items in ``list1`` and ``list2`` are separated by ``separator``, as are the items in the returned list.
''')

def evaluate(self, formatter, kwargs, mi, locals, list1, list2, separator):
l1 = [l.strip() for l in list1.split(separator) if l.strip()]
l2 = {icu_lower(l.strip()) for l in list2.split(separator) if l.strip()}

res = []
for i in l1:
if icu_lower(i) not in l2 and i not in res:
res.append(i)
if separator == ',':
return ', '.join(res)
return separator.join(res)

[문서]
class BuiltinListIntersection(BuiltinFormatterFunction):
name = 'list_intersection'
arg_count = 3 category = LIST_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``list_intersection(list1, list2, separator)`` -- return a list made by removing from ``list1`` any item not found in ``list2``[/] using a case-insensitive comparison. The items in ``list1`` and ``list2`` are separated by ``separator``, as are the items in the returned list.
''')

def evaluate(self, formatter, kwargs, mi, locals, list1, list2, separator):
l1 = [l.strip() for l in list1.split(separator) if l.strip()]
l2 = {icu_lower(l.strip()) for l in list2.split(separator) if l.strip()}

res = []
for i in l1:
if icu_lower(i) in l2 and i not in res:
res.append(i)
if separator == ',':
return ', '.join(res)
return separator.join(res)

[문서]
class BuiltinListSort(BuiltinFormatterFunction):
name = 'list_sort'
arg_count = 3 category = LIST_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``list_sort(value, direction, separator)`` -- return ``value`` sorted using a case-insensitive lexical sort.[/] If ``direction`` is zero (number or character),
``value`` is sorted ascending, otherwise descending. The list items are separated by ``separator``, as are the items in the returned list.
''')

def evaluate(self, formatter, kwargs, mi, locals, value, direction, separator):
res = [l.strip() for l in value.split(separator) if l.strip()]
if separator == ',':
return ', '.join(sorted(res, key=sort_key, reverse=direction != '0'))
return separator.join(sorted(res, key=sort_key, reverse=direction != '0'))

[문서]
class BuiltinListEquals(BuiltinFormatterFunction):
name = 'list_equals'
arg_count = 6 category = LIST_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``list_equals(list1, sep1, list2, sep2, yes_val, no_val)`` -- return ``yes_val`` if ``list1`` and ``list2`` contain the same items, otherwise return ``no_val``.[/]
The items are determined by splitting each list using the appropriate separator character (``sep1`` or ``sep2``). The order of items in the lists is not relevant. The comparison is case-insensitive.
''')

def evaluate(self, formatter, kwargs, mi, locals, list1, sep1, list2, sep2, yes_val, no_val):
s1 = {icu_lower(l.strip()) for l in list1.split(sep1) if l.strip()}
s2 = {icu_lower(l.strip()) for l in list2.split(sep2) if l.strip()}
if s1 == s2:
return yes_val return no_val

[문서]
class BuiltinListRe(BuiltinFormatterFunction):
name = 'list_re'
arg_count = 4 category = LIST_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``list_re(src_list, separator, include_re, opt_replace)`` -- Construct a list by first separating ``src_list`` into items using the ``separator`` character.[/] For each item in the list, check if it matches ``include_re``. If it does then add it to the list to be returned. If ``opt_replace`` is not the empty string then apply the replacement before adding the item to the returned list.
''')

def evaluate(self, formatter, kwargs, mi, locals, src_list, separator, include_re, opt_replace):
l = [l.strip() for l in src_list.split(separator) if l.strip()]
res = []
for item in l:
if re.search(include_re, item, flags=re.I) is not None:
if opt_replace:
item = re.sub(include_re, opt_replace, item)
for i in [t.strip() for t in item.split(separator) if t.strip()]:
if i not in res:
res.append(i)
if separator == ',':
return ', '.join(res)
return separator.join(res)

[문서]
class BuiltinListReGroup(BuiltinFormatterFunction):
name = 'list_re_group'
arg_count = -1 category = LIST_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``list_re_group(src_list, separator, include_re, search_re [,template_for_group]*)``
-- Like ``list_re()`` except replacements are not optional.[/] It uses ``re_group(item, search_re, template ...)`` when doing the replacements.
''')

def evaluate(self, formatter, kwargs, mi, locals, src_list, separator, include_re, search_re, *args):
from calibre.utils.formatter import EvalFormatter l = [l.strip() for l in src_list.split(separator) if l.strip()]
res = []
for item in l:
def repl(mo):
newval = ''
if mo and mo.lastindex:
for dex in range(mo.lastindex):
gv = mo.group(dex+1)
if gv is None:
continue if len(args) > dex:
template = args[dex].replace('[[', '{').replace(']]', '}')
newval += EvalFormatter().safe_format(template, {'$': gv},
'EVAL', None, strip_results=False)
else:
newval += gv return newval if re.search(include_re, item, flags=re.I) is not None:
item = re.sub(search_re, repl, item, flags=re.I)
for i in [t.strip() for t in item.split(separator) if t.strip()]:
if i not in res:
res.append(i)
if separator == ',':
return ', '.join(res)
return separator.join(res)

[문서]
class BuiltinToday(BuiltinFormatterFunction):
name = 'today'
arg_count = 0 category = DATE_FUNCTIONS def __doc__getter__(self): return translate_ffml( r'''
``today()`` -- return a date+time string for today (now).[/] This value is designed for use in ``format_date`` or ``days_between``, but can be manipulated like any other string. The date is in [URL href="https://en.wikipedia.org/wiki/ISO_8601"]ISO[/URL]
date/time format.
''')

def evaluate(self, formatter, kwargs, mi, locals):
return format_date(now(), 'iso')

[문서]
class BuiltinDaysBetween(BuiltinFormatterFunction):
name = 'days_between'
arg_count = 2 category = DATE_FUNCTIONS def __doc__getter__(self): return translate_ffml( r'''
``days_between(date1, date2)`` -- return the number of days between ``date1`` and ``date2``.[/] The number is positive if ``date1`` is greater than ``date2``, otherwise negative. If either ``date1`` or ``date2`` are not dates, the function returns the empty string.
''')

def evaluate(self, formatter, kwargs, mi, locals, date1, date2):
try:
d1 = parse_date(date1)
if d1 == UNDEFINED_DATE:
return ''
d2 = parse_date(date2)
if d2 == UNDEFINED_DATE:
return ''
except Exception:
return ''
i = d1 - d2 return f'{i.days+(i.seconds/(24.0*60.0*60.0)):.1f}'

[문서]
class BuiltinDateArithmetic(BuiltinFormatterFunction):
name = 'date_arithmetic'
arg_count = -1 category = DATE_FUNCTIONS def __doc__getter__(self): return translate_ffml( r'''
``date_arithmetic(value, calc_spec, fmt)`` -- Calculate a new date from ``value`` using ``calc_spec``.[/] Return the new date formatted according to optional
``fmt``: if not supplied then the result will be in ISO format. The ``calc_spec`` is a string formed by concatenating pairs of ``vW`` (``valueWhat``) where ``v`` is a possibly-negative number and W is one of the following letters:
[LIST]
[*]``s``: add ``v`` seconds to ``date``
[*]``m``: add ``v`` minutes to ``date``
[*]``h``: add ``v`` hours to ``date``
[*]``d``: add ``v`` days to ``date``
[*]``w``: add ``v`` weeks to ``date``
[*]``y``: add ``v`` years to ``date``, where a year is 365 days.
[/LIST]
Example: ``'1s3d-1m'`` will add 1 second, add 3 days, and subtract 1 minute from ``date``.
''')

calc_ops = {
's': lambda v: timedelta(seconds=v),
'm': lambda v: timedelta(minutes=v),
'h': lambda v: timedelta(hours=v),
'd': lambda v: timedelta(days=v),
'w': lambda v: timedelta(weeks=v),
'y': lambda v: timedelta(days=v * 365),
}

def evaluate(self, formatter, kwargs, mi, locals, value, calc_spec, fmt=None):
try:
d = parse_date(value)
if d == UNDEFINED_DATE:
return ''
while calc_spec:
mo = re.match(r'([-+\d]+)([smhdwy])', calc_spec)
if mo is None:
raise ValueError(
_("{0}: invalid calculation specifier '{1}'").format(
'date_arithmetic', calc_spec))
d += self.calc_ops[mo[2]](int(mo[1]))
calc_spec = calc_spec[len(mo[0]):]
return format_date(d, fmt or 'iso')
except ValueError as e:
raise e except Exception as e:
traceback.print_exc()
raise ValueError(_('{0}: error: {1}').format('date_arithmetic', str(e)))

[문서]
class BuiltinLanguageStrings(BuiltinFormatterFunction):
name = 'language_strings'
arg_count = 2 category = GET_FROM_METADATA def __doc__getter__(self): return translate_ffml( r'''
``language_strings(value, localize)`` -- return the language names for the language codes
([URL href="https://www.loc.gov/standards/iso639-2/php/code_list.php"]
see here for names and codes[/URL])
passed in ``value``.[/] Example: ``{languages:language_strings()}``.
If ``localize`` is zero, return the strings in English. If ``localize`` is not zero, return the strings in the language of the current locale. ``lang_codes`` is a comma-separated list.
''')

def evaluate(self, formatter, kwargs, mi, locals, lang_codes, localize):
retval = []
for c in [c.strip() for c in lang_codes.split(',') if c.strip()]:
try:
n = calibre_langcode_to_name(c, localize != '0')
if n:
retval.append(n)
except Exception:
pass return ', '.join(retval)

[문서]
class BuiltinLanguageCodes(BuiltinFormatterFunction):
name = 'language_codes'
arg_count = 1 category = GET_FROM_METADATA def __doc__getter__(self): return translate_ffml( r'''
``language_codes(lang_strings)`` -- return the
[URL href="https://www.loc.gov/standards/iso639-2/php/code_list.php"]language codes[/URL] for the language names passed in ``lang_strings``.[/] The strings must be in the language of the current locale. ``lang_strings`` is a comma-separated list.
''')

def evaluate(self, formatter, kwargs, mi, locals, lang_strings):
retval = []
for c in [c.strip() for c in lang_strings.split(',') if c.strip()]:
try:
cv = canonicalize_lang(c)
if cv:
retval.append(canonicalize_lang(cv))
except Exception:
pass return ', '.join(retval)

[문서]
class BuiltinCurrentLibraryName(BuiltinFormatterFunction):
name = 'current_library_name'
arg_count = 0 category = GET_FROM_METADATA def __doc__getter__(self): return translate_ffml( r'''
``current_library_name()`` -- return the last name on the path to the current calibre library.
''')

def evaluate(self, formatter, kwargs, mi, locals):
from calibre.library import current_library_name return current_library_name()

[문서]
class BuiltinCurrentLibraryPath(BuiltinFormatterFunction):
name = 'current_library_path'
arg_count = 0 category = GET_FROM_METADATA def __doc__getter__(self): return translate_ffml( r'''
``current_library_path()`` -- return the full path to the current calibre library.
''')

def evaluate(self, formatter, kwargs, mi, locals):
from calibre.library import current_library_path return current_library_path()

[문서]
class BuiltinFinishFormatting(BuiltinFormatterFunction):
name = 'finish_formatting'
arg_count = 4 category = FORMATTING_VALUES def __doc__getter__(self): return translate_ffml( r'''
``finish_formatting(value, format, prefix, suffix)`` -- apply the ``format``, ``prefix``, and
``suffix`` to the ``value`` in the same way as done in a template like
``{series_index:05.2f| - |- }``.[/] This function is provided to ease conversion of complex single-function- or template-program-mode templates to `GPM` Templates.
For example, the following program produces the same output as the above template:
[CODE]
program: finish_formatting(field("series_index"), "05.2f", " - ", " - ")
[/CODE]
Another example: for the template:
[CODE]
{series:re(([^\s])[^\s]+(\s|$),\1)}{series_index:0>2s| - | - }{title}
[/CODE]
use:
[CODE]
program:
strcat( re(field('series'), '([^\s])[^\s]+(\s|$)', '\1'), finish_formatting(field('series_index'), '0>2s', ' - ', ' - '), field('title')
)
[/CODE]
''')

def evaluate(self, formatter, kwargs, mi, locals_, val, fmt, prefix, suffix):
if not val:
return val return prefix + formatter._do_format(val, fmt) + suffix

[문서]
class BuiltinVirtualLibraries(BuiltinFormatterFunction):
name = 'virtual_libraries'
arg_count = 0 category = GET_FROM_METADATA def __doc__getter__(self): return translate_ffml( r'''
``virtual_libraries()`` -- return a comma-separated list of Virtual libraries that contain this book.[/] This function works only in the GUI. If you want to use these values in save-to-disk or send-to-device templates then you must make a custom
`Column built from other columns`, use the function in that column's template, and use that column's value in your save/send templates.
''')

def evaluate(self, formatter, kwargs, mi, locals_):
db = self.get_database(mi, formatter=formatter)
try:
a = db.data.get_virtual_libraries_for_books((mi.id,))
return ', '.join(a[mi.id])
except ValueError as v:
return str(v)

[문서]
class BuiltinCurrentVirtualLibraryName(BuiltinFormatterFunction):
name = 'current_virtual_library_name'
arg_count = 0 category = GET_FROM_METADATA def __doc__getter__(self): return translate_ffml( r'''
``current_virtual_library_name()`` -- return the name of the current virtual library if there is one, otherwise the empty string.[/] Library name case is preserved. Example:
[CODE]
program: current_virtual_library_name()
[/CODE]
This function works only in the GUI.
''')

def evaluate(self, formatter, kwargs, mi, locals):
return self.get_database(mi, formatter=formatter).data.get_base_restriction_name()

[문서]
class BuiltinUserCategories(BuiltinFormatterFunction):
name = 'user_categories'
arg_count = 0 category = GET_FROM_METADATA def __doc__getter__(self): return translate_ffml( r'''
``user_categories()`` -- return a comma-separated list of the user categories that contain this book.[/] This function works only in the GUI. If you want to use these values in save-to-disk or send-to-device templates then you must make a custom
`Column built from other columns`, use the function in that column's template, and use that column's value in your save/send templates
''')

def evaluate(self, formatter, kwargs, mi, locals_):
if hasattr(mi, '_proxy_metadata'):
cats = {k for k, v in mi._proxy_metadata.user_categories.items() if v}
cats = sorted(cats, key=sort_key)
return ', '.join(cats)
self.only_in_gui_error()

[문서]
class BuiltinTransliterate(BuiltinFormatterFunction):
name = 'transliterate'
arg_count = 1 category = STRING_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``transliterate(value)`` -- Return a string in a latin alphabet formed by approximating the sound of the words in ``value``.[/] For example, if ``value`` is ``{0}`` this function returns ``{1}``.
''').format('Фёдор Миха́йлович Достоевский', 'Fiodor Mikhailovich Dostoievskii')

def evaluate(self, formatter, kwargs, mi, locals, source):
from calibre.utils.filenames import ascii_text return ascii_text(source)

[문서]
class BuiltinGetLink(BuiltinFormatterFunction):
name = 'get_link'
arg_count = 2 category = DB_FUNCS def __doc__getter__(self): return translate_ffml( r'''
``get_link(field_name, field_value)`` -- fetch the link for field ``field_name`` with value ``field_value``.[/] If there is no attached link, return the empty string. Examples:
[LIST]
[*]The following returns the link attached to the tag ``Fiction``:
[CODE]
get_link('tags', 'Fiction')
[/CODE]
[*]This template makes a list of the links for all the tags associated with a book in the form ``value:link, ...``:
[CODE]
program:
ans = ''; for t in $tags:
l = get_link('tags', t); if l then ans = list_join(', ', ans, ',', t & ':' & get_link('tags', t), ',')
fi rof; ans
[/CODE]
[/LIST]
This function works only in the GUI and the content server.
''')

def evaluate(self, formatter, kwargs, mi, locals, field_name, field_value):
db = self.get_database(mi, formatter=formatter).new_api try:
link = None item_id = db.get_item_id(field_name, field_value, case_sensitive=True)
if item_id is not None:
link = db.link_for(field_name, item_id)
return link if link is not None else ''
except Exception as e:
traceback.print_exc()
raise ValueError(e)

[문서]
class BuiltinAuthorLinks(BuiltinFormatterFunction):
name = 'author_links'
arg_count = 2 category = GET_FROM_METADATA def __doc__getter__(self): return translate_ffml( r'''
``author_links(val_separator, pair_separator)`` -- returns a string containing a list of authors and those authors' link values[/] in the form:
``author1 val_separator author1_link pair_separator author2 val_separator author2_link`` etc.

An author is separated from its link value by the ``val_separator`` string with no added spaces. Assuming the ``val_separator`` is a colon,
``author:link value`` pairs are separated by the
``pair_separator`` string argument with no added spaces. It is up to you to choose separators that do not occur in author names or links. An author is included even if the author link is empty.
''')

def evaluate(self, formatter, kwargs, mi, locals, val_sep, pair_sep):
if hasattr(mi, '_proxy_metadata'):
link_data = mi._proxy_metadata.link_maps if not link_data:
return ''
link_data = link_data.get('authors')
if not link_data:
return ''
names = sorted(link_data.keys(), key=sort_key)
return pair_sep.join(n + val_sep + link_data[n] for n in names)
self.only_in_gui_error()

[문서]
class BuiltinAuthorSorts(BuiltinFormatterFunction):
name = 'author_sorts'
arg_count = 1 category = GET_FROM_METADATA def __doc__getter__(self): return translate_ffml( r'''
``author_sorts(val_separator)`` -- returns a string containing a list of author's sort values for the authors of the book.[/] The sort is the one in the author metadata information, which can be different from the author_sort in books. The returned list has the form ``author sort 1`` ``val_separator`` ``author sort 2`` etc. with no added spaces. The author sort values in this list are in the same order as the authors of the book. If you want spaces around ``val_separator`` then include them in the ``val_separator`` string.
''')

def evaluate(self, formatter, kwargs, mi, locals, val_sep):
sort_data = mi.author_sort_map if not sort_data:
return ''
names = [sort_data.get(n) for n in mi.authors if n.strip()]
return val_sep.join(n for n in names)

[문서]
class BuiltinConnectedDeviceName(BuiltinFormatterFunction):
name = 'connected_device_name'
arg_count = 1 category = GET_FROM_METADATA def __doc__getter__(self): return translate_ffml( r'''
``connected_device_name(storage_location_key)`` -- if a device is connected then return the device name, otherwise return the empty string.[/] Each storage location on a device has its own device name. The ``storage_location_key`` names are
``'main'``, ``'carda'`` and ``'cardb'``. This function works only in the GUI.
''')

def evaluate(self, formatter, kwargs, mi, locals, storage_location):
# We can't use get_database() here because we need the device manager.
# In other words, the function really does need the GUI with suppress(Exception):
# Do the import here so that we don't entangle the GUI when using
# command line functions from calibre.gui2.ui import get_gui info = get_gui().device_manager.get_current_device_information()
if info is None:
return ''
try:
if storage_location not in {'main', 'carda', 'cardb'}:
raise ValueError(
_('connected_device_name: invalid storage location "{}"').format(storage_location))
info = info['info'][4]
if storage_location not in info:
return ''
return info[storage_location]['device_name']
except Exception:
traceback.print_exc()
raise self.only_in_gui_error()

[문서]
class BuiltinConnectedDeviceUUID(BuiltinFormatterFunction):
name = 'connected_device_uuid'
arg_count = 1 category = GET_FROM_METADATA def __doc__getter__(self): return translate_ffml( r'''
``connected_device_uuid(storage_location_key)`` -- if a device is connected then return the device uuid (unique id), otherwise return the empty string.[/] Each storage location on a device has a different uuid. The ``storage_location_key`` location names are ``'main'``, ``'carda'`` and ``'cardb'``. This function works only in the GUI.
''')

def evaluate(self, formatter, kwargs, mi, locals, storage_location):
# We can't use get_database() here because we need the device manager.
# In other words, the function really does need the GUI with suppress(Exception):
# Do the import here so that we don't entangle the GUI when using
# command line functions from calibre.gui2.ui import get_gui info = get_gui().device_manager.get_current_device_information()
if info is None:
return ''
try:
if storage_location not in {'main', 'carda', 'cardb'}:
raise ValueError(
_('connected_device_name: invalid storage location "{}"').format(storage_location))
info = info['info'][4]
if storage_location not in info:
return ''
return info[storage_location]['device_store_uuid']
except Exception:
traceback.print_exc()
raise self.only_in_gui_error()

[문서]
class BuiltinCheckYesNo(BuiltinFormatterFunction):
name = 'check_yes_no'
arg_count = 4 category = STRING_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``check_yes_no(field_name, is_undefined, is_false, is_true)`` -- checks if the value of the yes/no field named by the lookup name ``field_name`` is one of the values specified by the parameters[/], returning ``'Yes'`` if a match is found otherwise returning the empty string. Set the parameter ``is_undefined``,
``is_false``, or ``is_true`` to 1 (the number) to check that condition, otherwise set it to 0.

Example: ``check_yes_no("#bool", 1, 0, 1)`` returns ``'Yes'`` if the yes/no field
``#bool`` is either True or undefined (neither True nor False).

More than one of ``is_undefined``, ``is_false``, or ``is_true`` can be set to 1.
''')

def evaluate(self, formatter, kwargs, mi, locals, field, is_undefined, is_false, is_true):
res = getattr(mi, field, None)
# Missing fields will return None. Oh well, this lets it be used everywhere,
# not just in the GUI.
if res is None:
if is_undefined == '1':
return 'Yes'
return ''
if not isinstance(res, bool):
raise ValueError(_('check_yes_no requires the field be a Yes/No custom column'))
if is_false == '1' and not res:
return 'Yes'
if is_true == '1' and res:
return 'Yes'
return ''

[문서]
class BuiltinRatingToStars(BuiltinFormatterFunction):
name = 'rating_to_stars'
arg_count = 2 category = FORMATTING_VALUES def __doc__getter__(self): return translate_ffml( r'''
``rating_to_stars(value, use_half_stars)`` -- Returns the ``value`` as string of star
(``{}``) characters.[/] The value must be a number between ``0`` and ``5``. Set
``use_half_stars`` to ``1`` if you want half star characters for fractional numbers available with custom ratings columns.
''').format('★')

def evaluate(self, formatter, kwargs, mi, locals, value, use_half_stars):
if not value:
return ''
err_msg = translate_ffml('The rating must be a number between 0 and 5')
try:
v = float(value) * 2 except Exception:
raise ValueError(err_msg)
if v < 0 or v > 10:
raise ValueError(err_msg)
from calibre.ebooks.metadata import rating_to_stars return rating_to_stars(v, use_half_stars == '1')

[문서]
class BuiltinSwapAroundArticles(BuiltinFormatterFunction):
name = 'swap_around_articles'
arg_count = 2 category = STRING_MANIPULATION def __doc__getter__(self): return translate_ffml( r''' ``swap_around_articles(value, separator)`` -- returns the ``value`` with articles moved to the end, separated by a semicolon.[/] The ``value`` can be a list, in which case each item in the list is processed. If the ``value`` is a list then you must provide the ``separator``. If no ``separator`` is provided or the separator is the empty string then the ``value`` is treated as being a single value, not a list. The `articles` are those used by calibre to generate the ``title_sort``.
''')

def evaluate(self, formatter, kwargs, mi, locals, val, separator):
if not val:
return ''
if not separator:
return title_sort(val).replace(',', ';')
result = []
try:
for v in [x.strip() for x in val.split(separator)]:
result.append(title_sort(v).replace(',', ';'))
except Exception:
traceback.print_exc()
return separator.join(sorted(result, key=sort_key))

[문서]
class BuiltinArguments(BuiltinFormatterFunction):
name = 'arguments'
arg_count = -1 category = OTHER def __doc__getter__(self): return translate_ffml( r'''
``arguments(id[=expression] [, id[=expression]]*)`` -- Used in a stored template to retrieve the arguments passed in the call.[/] It both declares and initializes local variables with the supplied names, the ``id``s, making them effectively parameters. The variables are positional; they get the value of the argument given in the call in the same position. If the corresponding argument is not provided in the call then ``arguments()`` assigns that variable the provided default value. If there is no default value then the variable is set to the empty string.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
# The arguments function is implemented in-line in the formatter raise NotImplementedError()

[문서]
class BuiltinGlobals(BuiltinFormatterFunction):
name = 'globals'
arg_count = -1 category = OTHER def __doc__getter__(self): return translate_ffml( r'''
``globals(id[=expression] [, id[=expression]]*)`` -- Retrieves "global variables"
that can be passed into the formatter.[/] The name ``id`` is the name of the global variable. It both declares and initializes local variables with the names of the global variables passed in the ``id`` parameters. If the corresponding variable is not provided in the globals then it assigns that variable the provided default value. If there is no default value then the variable is set to the empty string.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
# The globals function is implemented in-line in the formatter raise NotImplementedError()

[문서]
class BuiltinSetGlobals(BuiltinFormatterFunction):
name = 'set_globals'
arg_count = -1 category = OTHER def __doc__getter__(self): return translate_ffml( r'''
``set_globals(id[=expression] [, id[=expression]]*)`` -- Sets `global variables` that can be passed into the formatter.[/] The globals are given the name of the ``id`` passed in. The value of the ``id`` is used unless an expression is provided.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
# The globals function is implemented in-line in the formatter raise NotImplementedError()

[문서]
class BuiltinFieldExists(BuiltinFormatterFunction):
name = 'field_exists'
arg_count = 1 category = STRING_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``field_exists(lookup_name)`` -- checks if a field (column) with the lookup name
``lookup_name`` exists, returning ``'1'`` if so and the empty string if not.
''')

def evaluate(self, formatter, kwargs, mi, locals, field_name):
if field_name.lower() in mi.all_field_keys():
return '1'
return ''

[문서]
class BuiltinCharacter(BuiltinFormatterFunction):
name = 'character'
arg_count = 1 category = STRING_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``character(character_name)`` -- returns the character named by character_name.[/]
For example, ``character('newline')`` returns a newline character (``'\n'``).
The supported character names are ``newline``, ``return``, ``tab``, and
``backslash``. This function is used to put these characters into the output of templates.
''')

def evaluate(self, formatter, kwargs, mi, locals, character_name):
# The globals function is implemented in-line in the formatter raise NotImplementedError()

[문서]
class BuiltinToHex(BuiltinFormatterFunction):
name = 'to_hex'
arg_count = 1 category = URL_FUNCTIONS def __doc__getter__(self): return translate_ffml( r'''
``to_hex(val)`` -- returns the string ``val`` encoded into hex.[/] This is useful when constructing calibre URLs.
''')

def evaluate(self, formatter, kwargs, mi, locals, val):
return val.encode().hex()

[문서]
class BuiltinUrlsFromIdentifiers(BuiltinFormatterFunction):
name = 'urls_from_identifiers'
arg_count = 2 category = URL_FUNCTIONS def __doc__getter__(self): return translate_ffml( r'''
``urls_from_identifiers(identifiers, sort_results)`` -- given a comma-separated list of ``identifiers``, where an ``identifier`` is a colon-separated pair of values (``id_name:id_value``), returns a comma-separated list of HTML URLs generated from the identifiers.[/] The list not sorted if ``sort_results`` is ``0``
(character or number), otherwise it is sorted alphabetically by the identifier name. The URLs are generated in the same way as the built-in identifiers column when shown in Book Details.
''')

def evaluate(self, formatter, kwargs, mi, locals, identifiers, sort_results):
from calibre.ebooks.metadata.sources.identify import urls_from_identifiers try:
v = {}
for id_ in identifiers.split(','):
if id_:
pair = id_.split(':', maxsplit=1)
if len(pair) == 2:
l = pair[0].strip()
r = pair[1].strip()
if l and r:
v[l] = r urls = urls_from_identifiers(v, sort_results=str(sort_results) != '0')
p = prepare_string_for_xml a = partial(prepare_string_for_xml, attribute=True)
links = [f'<a href="{a(url)}" title="{a(id_typ)}:{a(id_val)}">{p(name)}</a>'
for name, id_typ, id_val, url in urls]
return ', '.join(links)
except Exception as e:
return str(e)[문서]
class BuiltinBookCount(BuiltinFormatterFunction):
name = 'book_count'
arg_count = 2 category = DB_FUNCS def __doc__getter__(self): return translate_ffml( r'''
``book_count(query, use_vl)`` -- returns the count of books found by searching for ``query``.[/] If ``use_vl`` is ``0`` (zero) then virtual libraries are ignored.
This function and its companion ``book_values()`` are particularly useful in template searches, supporting searches that combine information from many books such as looking for series with only one book. It cannot be used in composite columns unless the tweak ``allow_template_database_functions_in_composites`` is set to True. It can be used only in the GUI.

For example this template search uses this function and its companion to find all series with only one book:
[LIST]
[*]Define a stored template (using :guilabel:`Preferences->Advanced->Template functions`)
named ``series_only_one_book`` (the name is arbitrary). The template is:
[CODE]
program:
vals = globals(vals=''); if !vals then all_series = book_values('series', 'series:true', ',', 0); for series in all_series:
if book_count('series:="' & series & '"', 0) == 1 then vals = list_join(',', vals, ',', series, ',')
fi rof; set_globals(vals)
fi; str_in_list(vals, ',', $series, 1, '')
[/CODE]
The first time the template runs (the first book checked) it stores the results of the database lookups in a ``global`` template variable named ``vals``. These results are used to check subsequent books without redoing the lookups.
[*] Use the stored template in a template search:
[CODE]
template:"program: series_only_one_book()#@#:n:1"
[/CODE]
Using a stored template instead of putting the template into the search eliminates problems caused by the requirement to escape quotes in search expressions.
[/LIST]
This function can be used only in the GUI and the content server.
''')

def evaluate(self, formatter, kwargs, mi, locals, query, use_vl):
from calibre.db.fields import rendering_composite_name if (not tweaks.get('allow_template_database_functions_in_composites', False) and formatter.global_vars.get(rendering_composite_name, None)):
raise ValueError(_('The book_count() function cannot be used in a composite column'))
db = self.get_database(mi, formatter=formatter)
try:
if use_vl == '0':
# use the new_api search that doesn't use virtual libraries to let
# the function work in content server icon rules.
ids = db.new_api.search(query, None)
else:
ids = db.search_getting_ids(query, None, use_virtual_library=True)
return str(len(ids))
except Exception:
traceback.print_exc()[문서]
class BuiltinBookValues(BuiltinFormatterFunction):
name = 'book_values'
arg_count = 4 category = DB_FUNCS def __doc__getter__(self): return translate_ffml( r'''
``book_values(column, query, sep, use_vl)`` -- returns a list of the unique values contained in the column ``column`` (a lookup name), separated by ``sep``, in the books found by searching for ``query``.[/] If ``use_vl`` is ``0`` (zero)
then virtual libraries are ignored. This function and its companion
``book_count()`` are particularly useful in template searches, supporting searches that combine information from many books such as looking for series with only one book. It cannot be used in composite columns unless the tweak
``allow_template_database_functions_in_composites`` is set to True. This function can be used only in the GUI and the content server.
''')

def evaluate(self, formatter, kwargs, mi, locals, column, query, sep, use_vl):
from calibre.db.fields import rendering_composite_name if (not tweaks.get('allow_template_database_functions_in_composites', False) and formatter.global_vars.get(rendering_composite_name, None)):
raise ValueError(_('The book_values() function cannot be used in a composite column'))
db = self.get_database(mi, formatter=formatter)
if column not in db.field_metadata:
raise ValueError(_("The column {} doesn't exist").format(column))
try:
if use_vl == '0':
ids = db.new_api.search(query, None)
else:
ids = db.search_getting_ids(query, None, use_virtual_library=True)
s = set()
for id_ in ids:
f = db.new_api.get_proxy_metadata(id_).get(column, None)
if isinstance(f, (tuple, list)):
s.update(f)
elif f is not None:
s.add(str(f))
return sep.join(s)
except Exception as e:
raise ValueError(e)[문서]
class BuiltinHasExtraFiles(BuiltinFormatterFunction):
name = 'has_extra_files'
arg_count = -1 category = DB_FUNCS def __doc__getter__(self): return translate_ffml( r'''
``has_extra_files([pattern])`` -- returns the count of extra files, otherwise ''
(the empty string).[/] If the optional parameter ``pattern`` (a regular expression)
is supplied then the list is filtered to files that match ``pattern`` before the files are counted. The pattern match is case insensitive. See also the functions
:ref:`extra_file_names`, :ref:`extra_file_size` and :ref:`extra_file_modtime`.
This function can be used only in the GUI and the content server.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
if len(args) > 1:
raise ValueError(_('Incorrect number of arguments for function {0}').format('has_extra_files'))
pattern = args[0] if len(args) == 1 else None db = self.get_database(mi, formatter=formatter).new_api try:
files = tuple(f.relpath.partition('/')[-1] for f in db.list_extra_files(mi.id, use_cache=True, pattern=DATA_FILE_PATTERN))
if pattern:
r = re.compile(pattern, re.IGNORECASE)
files = tuple(filter(r.search, files))
return len(files) if len(files) > 0 else ''
except Exception as e:
traceback.print_exc()
raise ValueError(e)[문서]
class BuiltinExtraFileNames(BuiltinFormatterFunction):
name = 'extra_file_names'
arg_count = -1 category = DB_FUNCS def __doc__getter__(self): return translate_ffml( r'''
``extra_file_names(sep [, pattern])`` -- returns a ``sep``-separated list of extra files in the book's ``data/`` folder.[/] If the optional parameter
``pattern``, a regular expression, is supplied then the list is filtered to files that match ``pattern``. The pattern match is case insensitive. See also the functions :ref:`has_extra_files`, :ref:`extra_file_modtime` and
:ref:`extra_file_size`. This function can be used only in the GUI and the content server.
''')

def evaluate(self, formatter, kwargs, mi, locals, sep, *args):
if len(args) > 1:
raise ValueError(_('Incorrect number of arguments for function {0}').format('has_extra_files'))
pattern = args[0] if len(args) == 1 else None db = self.get_database(mi, formatter=formatter).new_api try:
files = tuple(f.relpath.partition('/')[-1] for f in db.list_extra_files(mi.id, use_cache=True, pattern=DATA_FILE_PATTERN))
if pattern:
r = re.compile(pattern, re.IGNORECASE)
files = tuple(filter(r.search, files))
return sep.join(files)
except Exception as e:
traceback.print_exc()
raise ValueError(e)

[문서]
class BuiltinExtraFileSize(BuiltinFormatterFunction):
name = 'extra_file_size'
arg_count = 1 category = DB_FUNCS def __doc__getter__(self): return translate_ffml( r'''
``extra_file_size(file_name)`` -- returns the size in bytes of the extra file
``file_name`` in the book's ``data/`` folder if it exists, otherwise ``-1``.[/] See also the functions :ref:`has_extra_files`, :ref:`extra_file_names` and
:ref:`extra_file_modtime`. This function can be used only in the GUI and the content server.
''')

def evaluate(self, formatter, kwargs, mi, locals, file_name):
db = self.get_database(mi, formatter=formatter).new_api try:
q = posixpath.join(DATA_DIR_NAME, file_name)
for f in db.list_extra_files(mi.id, use_cache=True, pattern=DATA_FILE_PATTERN):
if f.relpath == q:
return str(f.stat_result.st_size)
return str(-1)
except Exception as e:
traceback.print_exc()
raise ValueError(e)

[문서]
class BuiltinExtraFileModtime(BuiltinFormatterFunction):
name = 'extra_file_modtime'
arg_count = 2 category = DB_FUNCS def __doc__getter__(self): return translate_ffml( r'''
``extra_file_modtime(file_name, format_string)`` -- returns the modification time of the extra file ``file_name`` in the book's ``data/`` folder[/] if it exists, otherwise ``-1``. The modtime is formatted according to
``format_string`` (see :ref:`format_date` for details). If ``format_string`` is the empty string, returns the modtime as the floating point number of seconds since the epoch. See also the functions :ref:`has_extra_files`,
:ref:`extra_file_names` and :ref:`extra_file_size`. The epoch is OS dependent.
This function can be used only in the GUI and the content server.
''')

def evaluate(self, formatter, kwargs, mi, locals, file_name, format_string):
db = self.get_database(mi, formatter=formatter).new_api try:
q = posixpath.join(DATA_DIR_NAME, file_name)
for f in db.list_extra_files(mi.id, use_cache=True, pattern=DATA_FILE_PATTERN):
if f.relpath == q:
val = f.stat_result.st_mtime if format_string:
return format_date(datetime.fromtimestamp(val), format_string)
return str(val)
return str(1.0)
except Exception as e:
traceback.print_exc()
raise ValueError(e)

[문서]
class BuiltinGetNote(BuiltinFormatterFunction):
name = 'get_note'
arg_count = 3 category = DB_FUNCS def __doc__getter__(self): return translate_ffml( r'''
``get_note(field_name, field_value, plain_text)`` -- fetch the note for field
``field_name`` with value ``field_value``.[/] If ``plain_text`` is empty, return the note's HTML including images. If ``plain_text`` is ``1`` (or ``'1'``), return the note's plain text. If the note doesn't exist, return the empty string in both cases. Example:
[LIST]
[*]Return the HTML of the note attached to the tag `Fiction`:
[CODE]
program:
get_note('tags', 'Fiction', '')
[/CODE]
[*]Return the plain text of the note attached to the author `Isaac Asimov`:
[CODE]
program:
get_note('authors', 'Isaac Asimov', 1)
[/CODE]
[/LIST]
This function works only in the GUI and the content server.
''')

def evaluate(self, formatter, kwargs, mi, locals, field_name, field_value, plain_text):
db = self.get_database(mi, formatter=formatter).new_api try:
note = None item_id = db.get_item_id(field_name, field_value, case_sensitive=True)
if item_id is not None:
note = db.notes_data_for(field_name, item_id)
if note is not None:
if plain_text == '1':
note = note['searchable_text'].partition('\n')[2]
else:
from lxml import html from calibre.db.notes.exim import expand_note_resources, parse_html
# Return the full HTML of the note, including all images
# as data: URLs. Reason: non-exported note html contains
# "calres://" URLs for images. These images won't render
# outside the context of the library where the note
# "lives". For example, they don't work in book jackets
# and book details from a different library. They also
# don't work in tooltips.

# This code depends on the note being wrapped in <body>
# tags by parse_html. The body is changed to a <div>.
# That means we often end up with <div><div> or some
# such, but that is OK root = parse_html(note['doc'])
# There should be only one <body> root = root.xpath('//body')[0]
# Change the body to a div root.tag = 'div'
# Expand all the resources in the note root = expand_note_resources(root, db.get_notes_resource)
note = html.tostring(root, encoding='unicode')
return '' if note is None else note except Exception as e:
traceback.print_exc()
raise ValueError(e)[문서]
class BuiltinHasNote(BuiltinFormatterFunction):
name = 'has_note'
arg_count = 2 category = DB_FUNCS def __doc__getter__(self): return translate_ffml( r'''
``has_note(field_name, field_value)``. Check if a field has a note.[/]
This function has two variants:
[LIST]
[*]if ``field_value`` is not ``''`` (the empty string) return ``'1'`` if the value ``field_value`` in the field ``field_name`` has a note, otherwise ``''``.

Example: ``has_note('tags', 'Fiction')`` returns ``'1'`` if the tag ``fiction`` has an attached note, otherwise ``''``.

[*]If ``field_value`` is ``''`` then return a list of values in ``field_name`` that have a note. If no item in the field has a note, return ``''``. This variant is useful for showing column icons if any value in the field has a note, rather than a specific value.

Example: ``has_note('authors', '')`` returns a list of authors that have notes, or
``''`` if no author has a note.
[/LIST]

You can test if all the values in ``field_name`` have a note by comparing the list length of this function's return value against the list length of the values in ``field_name``. Example:
[CODE]
list_count(has_note('authors', ''), '&') ==# list_count_field('authors')
[/CODE]
This function works only in the GUI and the content server.
''')

def evaluate(self, formatter, kwargs, mi, locals, field_name, field_value):
db = self.get_database(mi, formatter=formatter).new_api if field_value:
note = None try:
item_id = db.get_item_id(field_name, field_value, case_sensitive=True)
if item_id is not None:
note = db.notes_data_for(field_name, item_id)
except Exception as e:
traceback.print_exc()
raise ValueError(str(e))
return '1' if note is not None else ''
try:
notes_for_book = db.items_with_notes_in_book(mi.id)
values = list(notes_for_book.get(field_name, {}).values())
return db.field_metadata[field_name]['is_multiple'].get('list_to_ui', ', ').join(values)
except Exception as e:
traceback.print_exc()
raise ValueError(str(e))[문서]
class BuiltinReadingProgress(BuiltinFormatterFunction):
name = 'reading_progress'
arg_count = -1 category = DB_FUNCS def __doc__getter__(self): return translate_ffml( r'''
``reading_progress(book_id, [user, output_fmt, which, fmt])`` -- returns the reading progress, in the specified output format.[/]The ``user`` parameter defaults to match any user. Use the value ``local`` to match reading progress in the calibre e-book viewer. Use ``_`` to match reading progress for anonymous users of the Content server viewer.
Any other value matches the corresponding username as used in the Content server.

The ``output_fmt`` parameter controls the format of the text returned by this function. It takes three values:
[LIST]
[*] ``page_count`` - the default, outputs ``pages read / total pages``. If page counting is not enabled outputs percent read instead.
[*] ``percent`` - outputs percent read
[*] ``pos_frac`` - outputs a fraction between zero and one.
[/LIST]

The ``which`` parameter controls how the specific reading progress record for the specified ``user`` is selected.
There can be more than one record if no user is specified or if the book has been read in multiple formats or on multiple devices.
It accepts two values:
[LIST]
[*] ``most_recent`` - the progress of the most recent reader of the book (the default value)
[*] ``furthest`` - the furthest progress of all matching records
[/LIST]

The ``fmt`` parameter controls which book format is used. The default is to return records for all formats, the specific record is then selected by the ``which`` parameter.

Some examples:
[CODE]
{id:reading_progress()} -- the reading progress as pages read / total pages for the most recent reading session of this book
{id:reading_progress(,percent)} -- same as above, but as a percentage
{id:reading_progress(,pos_frac,furthest)} -- same as above, but as a fraction and using the furthest progress on this book.
{id:reading_progress(bob,pos_frac,furthest,EPUB)} -- for the user "bob" and the "EPUB format
[/CODE]
''')

def evaluate(self, formatter, kwargs, mi, locals, book_id, *args):
if len(args) > 4:
raise ValueError(_('Incorrect number of arguments for function {0}').format('reading_progress'))
book_id = int(book_id)
for_user, output_fmt, which, fmt = '', 'page_count', 'most_recent', ''
match len(args):
case 4:
for_user, output_fmt, which, fmt = args case 3:
for_user, output_fmt, which = args case 2:
for_user, output_fmt = args case 1:
for_user = args[0]
order_by = 'pos_frac' if which in ('furthest', 'farthest') else 'epoch'
pos_frac = 0 with (db := self.get_database(mi, formatter=formatter).new_api).safe_read_lock:
if records := db._get_last_read_positions(book_id, fmt=fmt, user=for_user, order_by=order_by, limit=1):
pos_frac = records[0]['pos_frac']
fmt = records[0]['format']
match output_fmt:
case 'percent':
return f'{pos_frac:.0%}'
case 'page_count':
page_count = 0 if pages := db._get_pages(book_id):
page_count = pages.pages if page_count > 0:
return f'{int(pos_frac * page_count)} / {page_count}'
return f'{pos_frac:.0%}'
case _:
return str(pos_frac)

[문서]
class BuiltinIsDarkMode(BuiltinFormatterFunction):
name = 'is_dark_mode'
arg_count = 0 category = OTHER def __doc__getter__(self): return translate_ffml( r'''
``is_dark_mode()`` -- returns ``'1'`` if calibre is running in dark mode, ``''``
(the empty string) otherwise.[/] This function can be used in advanced color and icon rules to choose different colors/icons according to the mode. Example:
[CODE]
if is_dark_mode() then 'dark.png' else 'light.png' fi
[/CODE]
''')

def evaluate(self, formatter, kwargs, mi, locals):
try:
# Import this here so that Qt isn't referenced unless this function is used.
from calibre.gui2 import is_dark_theme return '1' if is_dark_theme() else ''
except Exception:
only_in_gui_error('is_dark_mode')

[문서]
class BuiltinFieldListCount(BuiltinFormatterFunction):
name = 'list_count_field'
arg_count = 0 category = LIST_MANIPULATION def __doc__getter__(self): return translate_ffml( r'''
``list_count_field(lookup_name)``-- returns the count of items in the field with the lookup name ``lookup_name``.[/] The field must be multi-valued such as
``authors`` or ``tags``, otherwise the function raises an error. This function is much faster than ``list_count()`` because it operates directly on calibre data without converting it to a string first. Example: ``list_count_field('tags')``.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
# The globals function is implemented in-line in the formatter raise NotImplementedError()

[문서]
class BuiltinMakeUrl(BuiltinFormatterFunction):
name = 'make_url'
arg_count = -1 category = URL_FUNCTIONS def __doc__getter__(self): return translate_ffml( r'''
``make_url(path, [query_name, query_value]+)`` -- this function is the easiest way to construct a query URL. It uses a ``path``, the web site and page you want to query, and ``query_name``, ``query_value`` pairs from which the query is built.
In general, the ``query_value`` must be URL-encoded. With this function it is always encoded and spaces are always replaced with ``'+'`` signs.[/]

At least one ``query_name, query_value`` pair must be provided.

Example: constructing a Wikipedia search URL for the author `{0}`:
[CODE]
make_url('https://en.wikipedia.org/w/index.php', 'search', '{0}')
[/CODE]
returns
[CODE]
https://en.wikipedia.org/w/index.php?search=Niccol%C3%B2+Machiavelli
[/CODE]

If you are writing a custom column book details URL template then use ``$item_name`` or
``field('item_name')`` to obtain the value of the field that was clicked on.
Example: if `{0}` was clicked then you can construct the URL using:
[CODE]
make_url('https://en.wikipedia.org/w/index.php', 'search', $item_name)
[/CODE]

See also the functions :ref:`make_url_extended`, :ref:`query_string` and :ref:`encode_for_url`.
''').format('Niccolò Machiavelli') # not translated ans gettext wants pure ascii msgid def evaluate(self, formatter, kwargs, mi, locals, path, *args):
if (len(args) % 2) != 0:
raise ValueError(_('{} requires an odd number of arguments').format('make_url'))
if len(args) < 2:
raise ValueError(_('{} requires at least 3 arguments').format('make_url'))
query_args = []
for i in range(0, len(args), 2):
query_args.append(f'{args[i]}={qquote(args[i+1].strip())}')
return f'{path}?{"&".join(query_args)}'[문서]
class BuiltinMakeUrlExtended(BuiltinFormatterFunction):
name = 'make_url_extended'
arg_count = -1 category = URL_FUNCTIONS def __doc__getter__(self): return translate_ffml( r'''
``make_url_extended(...)`` -- this function is similar to :ref:`make_url` but gives you more control over the URL components. The components of a URL are

[B]scheme[/B]:://[B]authority[/B]/[B]path[/B]?[B]query string[/B].

See [URL href="https://en.wikipedia.org/wiki/URL"]Uniform Resource Locator[/URL] on Wikipedia for more detail.

The function has two variants:
[CODE]
make_url_extended(scheme, authority, path, [query_name, query_value]+)
[/CODE]
and
[CODE]
make_url_extended(scheme, authority, path, query_string)
[/CODE]
[/]
This function returns a URL constructed from the ``scheme``, ``authority``, ``path``, and either the ``query_string`` or a query string constructed from the query argument pairs.
The ``authority`` can be empty, which is the case for ``calibre`` scheme URLs.
You must supply either a ``query_string`` or at least one ``query_name, query_value`` pair.
If you supply ``query_string`` and it is empty then the resulting URL will not have a query string section.

Example 1: constructing a Wikipedia search URL for the author `{0}`:
[CODE]
make_url_extended('https', 'en.wikipedia.org', '/w/index.php', 'search', '{0}')
[/CODE]
returns
[CODE]
https://en.wikipedia.org/w/index.php?search=Niccol%C3%B2+Machiavelli
[/CODE]

See the :ref:`query_string` function for an example using ``make_url_extended()`` with a ``query_string``.

If you are writing a custom column book details URL template then use ``$item_name`` or
``field('item_name')`` to obtain the value of the field that was clicked on.
Example: if `{0}` was clicked on then you can construct the URL using :
[CODE]
make_url_extended('https', 'en.wikipedia.org', '/w/index.php', 'search', $item_name')
[/CODE]

See also the functions :ref:`make_url`, :ref:`query_string` and :ref:`encode_for_url`.
''').format('Niccolò Machiavelli') # not translated as gettext wants pure ASCII msgid def evaluate(self, formatter, kwargs, mi, locals, scheme, authority, path, *args):
if len(args) != 1:
if (len(args) % 2) != 0:
raise ValueError(_('{} requires an odd number of arguments').format('make_url_extended'))
if len(args) < 2:
raise ValueError(_('{} requires at least 5 arguments').format('make_url_extended'))
query_args = []
for i in range(0, len(args), 2):
query_args.append(f'{args[i]}={qquote(args[i+1].strip())}')
qs = '&'.join(query_args)
else:
qs = args[0]
if qs:
qs = '?' + qs return (f"{scheme}://{authority}{'/' if authority else ''}"
f"{path.removeprefix('/')}{qs}")[문서]
class BuiltinQueryString(BuiltinFormatterFunction):
name = 'query_string'
arg_count = -1 category = URL_FUNCTIONS def __doc__getter__(self): return translate_ffml( r'''
``query_string([query_name, query_value, how_to_encode]+)``-- returns a URL query string constructed from the ``query_name, query_value, how_to_encode`` triads.
A query string is a series of items where each item looks like ``query_name=query_value`` where ``query_value`` is URL-encoded as instructed. The query items are separated by
``'&'`` (ampersand) characters.[/]

If ``how_to_encode`` is ``0`` then ``query_value`` is encoded and spaces are replaced with ``'+'`` (plus) signs. If ``how_to_encode`` is ``1`` then ``query_value`` is encoded with spaces replaced by ``%20``. If ``how_to_encode`` is ``2`` then ``query_value`` is returned unchanged; no encoding is done and spaces are not replaced. If you want
``query_value`` not to be encoded but spaces to be replaced then use the :ref:`re` function, as in ``re($series, ' ', '%20')``

You use this function if you need specific control over how the parts of the query string are constructed. You could then use the resultingquery string in
:ref:`make_url_extended`, as in
[CODE]
make_url_extended(
'https', 'your_host', 'your_path', query_string('encoded', '{0}', 0, 'unencoded', '{0}', 2))
[/CODE]
giving you
[CODE]
https://your_host/your_path?encoded=Hendrik+B%C3%A4%C3%9Fler&unencoded={0}
[/CODE]

You must have at least one ``query_name, query_value, how_to_encode`` triad, but can have as many as you wish.

The returned value is a URL query string with all the specified items, for example:
``name1=val1[&nameN=valN]*``. Note that the ``'?'`` `path` / `query string` separator is not included in the returned result.

If you are writing a custom column book details URL template then use ``$item_name`` or
``field('item_name')`` to obtain the unencoded value of the field that was clicked.
You also have ``item_value_quoted`` where the value is already encoded with plus signs replacing spaces, and ``item_value_no_plus`` where the value is already encoded with ``%20`` replacing spaces.

See also the functions :ref:`make_url`, :ref:`make_url_extended` and :ref:`encode_for_url`.
''').format('Hendrik Bäßler')

def evaluate(self, formatter, kwargs, mi, locals, *args):
if (len(args) % 3) != 0 or len(args) < 3:
raise ValueError(_('{} requires at least one group of 3 arguments').format('query_string'))
funcs = [ partial(qquote, use_plus=True), partial(qquote, use_plus=False), lambda x:x,
]
query_args = []
for i in range(0, len(args), 3):
if (f := args[i+2]) not in ('0', '1', '2'):
raise ValueError(
_('In {} the third argument of a group must be 0, 1, or 2, not {}').format('query_string', f))
query_args.append(f'{args[i]}={funcs[int(f)](args[i+1].strip())}')
return '&'.join(query_args)

[문서]
class BuiltinEncodeForURL(BuiltinFormatterFunction):
name = 'encode_for_url'
arg_count = 2 category = URL_FUNCTIONS def __doc__getter__(self): return translate_ffml( r'''
``encode_for_url(value, use_plus)`` -- returns the ``value`` encoded for use in a URL as specified by ``use_plus``. The value is first URL-encoded. Next, if ``use_plus`` is ``0`` then spaces are replaced by ``'+'`` (plus) signs. If it is ``1`` then spaces are replaced by ``%20``.[/]

If you do not want the value to be encoding but to have spaces replaced then use the
:ref:`re` function, as in ``re($series, ' ', '%20')``

See also the functions :ref:`make_url`, :ref:`make_url_extended` and :ref:`query_string`.
''')

def evaluate(self, formatter, kwargs, mi, locals, value, use_plus):
if use_plus not in ('0', '1'):
raise ValueError(
_('In {} the second argument must be 0, or 1, not {}').format('quote_for_url', use_plus))
return qquote(value, use_plus=use_plus=='0')

[문서]
class BuiltinFormatDuration(BuiltinFormatterFunction):
name = 'format_duration'
arg_count = -1 category = FORMATTING_VALUES def __doc__getter__(self): return translate_ffml( r'''
``format_duration(value, template, [largest_unit])`` -- format the value, a number of seconds, into a string showing weeks, days, hours, minutes, and seconds. If the value is a float then it is rounded to the nearest integer.[/] You choose how to format the value using a template consisting of value selectors surrounded by ``[`` and ``]`` characters. The selectors are:
[LIST]
[*]``[w]``: weeks
[*]``[d]``: days
[*]``[h]``: hours
[*]``[m]``: minutes
[*]``[s]``: seconds
[/LIST]
You can put arbitrary text between selectors.

The following examples use a duration of 2 days (172,800 seconds) 1 hour (3,600 seconds)
and 20 seconds, which totals to 176,420 seconds.
[LIST]
[*]``format_duration(176420, '[d][h][m][s]')`` will return the value ``2d 1h 0m 20s``.
[*]``format_duration(176420, '[h][m][s]')`` will return the value ``49h 0m 20s``.
[*]``format_duration(176420, 'Your reading time is [d][h][m][s]')`` returns the value
``Your reading time is 49h 0m 20s``.
[*]``format_duration(176420, '[w][d][h][m][s]')`` will return the value ``2d 1h 0m 20s``.
Note that the zero weeks value is not returned.
[/LIST]
If you want to see zero values for items such as weeks in the above example, use an uppercase selector. For example, the following uses ``'W'`` to show zero weeks:

``format_duration(176420, '[W][d][h][m][s]')`` returns ``0w 2d 1h 0m 20s``.

By default the text following a value is the selector followed by a space.
You can change that to whatever text you want. The format for a selector with your text is the selector followed by a colon followed by text segments separated by ``'|'`` characters. You must include any space characters you want in the output.

You can provide from one to three text segments.
[LIST]
[*]If you provide one segment, as in ``[w: weeks ]`` then that segment is used for all values.
[*]If you provide two segments, as in ``[w: weeks | week ]`` then the first segment is used for 0 and more than 1. The second segment is used for 1.
[*]If you provide three segments, as in ``[w: weeks | week | weeks ]`` then the first segment is used for 0, the second segment is used for 1, and the third segment is used for more than 1.
[/LIST]
The second form is equivalent to the third form in many languages.

For example, the selector:
[LIST]
[*]``[w: weeks | week | weeks ]`` produces ``'0 weeks '``, ``'1 week '``, or ``'2 weeks '``.
[*]``[w: weeks | week ]`` produces ``'0 weeks '``, ``'1 week '``, or ``'2 weeks '``.
[*]``[w: weeks ]`` produces ``0 weeks '``, ``1 weeks '``, or ``2 weeks '``.
[/LIST]

The optional ``largest_unit`` parameter specifies the largest of weeks, days, hours, minutes, and seconds that will be produced by the template. It must be one of the value selectors.
This can be useful to truncate a value.

``format_duration(176420, '[h][m][s]', 'd')`` will return the value ``1h 0m 20s`` instead of ``49h 0m 20s``.
''')

def evaluate(self, formatter, kwargs, mi, locals, value, template, largest_unit=''):
if largest_unit not in 'wdhms':
raise ValueError(_('the {0} parameter must be one of {1}').format('largest_unit', 'wdhms'))

pat = re.compile(r'\[(.)(:(.*?))?\]')

if not largest_unit:
highest_index = 0 for m in pat.finditer(template):
try:
# We know that m.group(1) is a single character so the only
# exception possible is that the character is not in the string dex = 'smhdw'.index(m.group(1).lower())
highest_index = max(highest_index, dex)
except Exception:
raise ValueError(_('The {} format specifier is not valid').format(m.group()))
largest_unit = 'smhdw'[highest_index]

int_val = remainder = round(float(value)) if value else 0 weeks,remainder = divmod(remainder, 60*60*24*7) if largest_unit == 'w' else (-1,remainder)
days,remainder = divmod(remainder, 60*60*24) if largest_unit in 'wd' else (-1,remainder)
hours,remainder = divmod(remainder, 60*60) if largest_unit in 'wdh' else (-1,remainder)
minutes,remainder = divmod(remainder, 60) if largest_unit in 'wdhm' else (-1,remainder)
seconds = remainder def repl(mo):
fmt_char = mo.group(1)
suffixes = mo.group(3)
if suffixes is None:
zero_suffix = one_suffix = more_suffix = fmt_char.lower() + ' '
else:
suffixes = re.split(r'\|', suffixes)
match len(suffixes):
case 1:
zero_suffix = one_suffix = more_suffix = suffixes[0]
case 2:
zero_suffix = more_suffix = suffixes[0]
one_suffix = suffixes[1]
case 3:
zero_suffix = suffixes[0]
one_suffix = suffixes[1]
more_suffix = suffixes[2]
case _:
raise ValueError(_('The group {} has too many suffixes').format(fmt_char))
zero_suffix = one_suffix = more_suffix = '@@too many suffixes@@'

def val_with_suffix(val, test_val):
match val:
case -1:
return ''
case 0 if fmt_char.islower() and int_val < test_val:
return ''
case 0:
return str(val) + zero_suffix case 1:
return str(val) + one_suffix case _:
return str(val) + more_suffix match fmt_char.lower():
case 'w':
return val_with_suffix(weeks, 60*60*24*7)
case 'd':
return val_with_suffix(days, 60*60*24)
case 'h':
return val_with_suffix(hours, 60*60)
case 'm':
return val_with_suffix(minutes, 60)
case 's':
return val_with_suffix(seconds, -1)
case _:
raise ValueError(_('The {} format specifier is not valid').format(fmt_char))

return pat.sub(repl, template)

[문서]
class BuiltinSelectedBooks(BuiltinFormatterFunction):
name = 'selected_books'
arg_count = 0 category = GUI_FUNCTIONS def __doc__getter__(self): return translate_ffml( r'''
``selected_books([sorted_by, ascending])`` -- returns a list of book ids in selection order for the currently selected books.

This function can be used only in the GUI.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
from calibre.gui2.ui import get_gui g = get_gui()
book_ids = g.current_view().get_selected_ids()
return ', '.join([str(book_id) for book_id in book_ids])

[문서]
class BuiltinSortBookIds(BuiltinFormatterFunction):
name = 'sort_book_ids'
arg_count = -1 category = GUI_FUNCTIONS def __doc__getter__(self): return translate_ffml( r'''
``sort_book_ids(book_ids, sorted_by, ascending [, sorted_by, ascending]*)`` -- returns the list of book ids sorted by the column specified by the lookup name in ``sorted_by`` in the order specified by ``ascending``. If ``ascending`` is
``'1'`` then the books are sorted by the value in the 'sorted_by' column in ascending order, otherwise in descending order. You can have multiple pairs of
``sorted_by, ascending``. The first pair specifies the major order.

This function can be used only in the GUI.
''')

def evaluate(self, formatter, kwargs, mi, locals, book_ids, *args):
from calibre.gui2.ui import get_gui g = get_gui()
bids = [int(b.strip()) for b in book_ids.split(',')]
if len(args) < 2:
raise ValueError(_('The sort_book_ids function requires at least 3 arguments'))
if len(args) % 2 != 0:
raise ValueError(_('The id and direction arguments must be in pairs'))
sort_spec = []
for i in range(0, len(args), 2):
sort_by = args[i]
asc = True if args[i+1] == '1' else False sort_spec.append((sort_by, asc))
bids = g.current_db.new_api.multisort(sort_spec, bids)
return ', '.join([str(b) for b in bids])

[문서]
class BuiltinSelectedColumn(BuiltinFormatterFunction):
name = 'selected_column'
arg_count = 0 category = GUI_FUNCTIONS def __doc__getter__(self): return translate_ffml( r'''
``selected_column()`` -- returns the lookup name of the column containing the currently selected cell. It returns ``''`` if no cell is selected.

This function can be used only in the GUI.
''')

def evaluate(self, formatter, kwargs, mi, locals):
from calibre.gui2.ui import get_gui v = get_gui().current_view()
idx = v.currentIndex()
if idx.isValid():
key = v.column_map[idx.column()]
return key return ''

[문서]
class BuiltinWidthFromPages(BuiltinFormatterFunction):
name = 'width_from_pages'
arg_count = -1 category = GUI_FUNCTIONS def __doc__getter__(self): return translate_ffml( r'''
``width_from_pages(value [, num_of_pages_for_max_width, logarithmic_factor, default_width])`` -- return the width of the book spine as a fraction between ``'0'`` and ``'1'`` given a number of pages.
This is used to calculate the width of the spine in the Bookshelf view, from a page count. The optional arguments control how the width is calculated.

[LIST]
[*] ``num_of_pages_for_max_width`` -- controls the widest books, any book with at least the specified number of pages is given width 1. Defaults to ``1500``.
[*] ``logarithmic_factor`` -- controls how quickly width varies as pages range from 0 to the maximum. Defaults to ``2``.
[*] ``default_width`` -- is the width for books with an invalid number of pages.
[/LIST]
''')

def evaluate(self, formatter, kwargs, mi, locals, val, *args):
from calibre.gui2.library.bookshelf_view import width_from_pages num_of_pages_for_max_width = 1500 logarithmic_factor = 2 default_width = '0.3'
match len(args):
case 1:
if args[0]:
num_of_pages_for_max_width = int(args[0])
case 2:
num_of_pages_for_max_width, logarithmic_factor = int(args[0]), float(args[1])
case 3:
num_of_pages_for_max_width, logarithmic_factor, default_width = int(args[0]), float(args[1]), args[2]
try:
pages = int(val)
except Exception:
return default_width if pages < 0:
return default_width return str(width_from_pages(pages, num_of_pages_for_max_width, logarithmic_factor))

[문서]
class BuiltinShowDialog(BuiltinFormatterFunction):
name = 'show_dialog'
arg_count = 1 category = GUI_FUNCTIONS def __doc__getter__(self): return translate_ffml( r'''
``show_dialog(html_or_text)`` -- show a dialog containing the html or text. The function returns ``'1'`` if the user presses OK, ``''`` if Cancel.

This function can be used only in the GUI.
''')

def evaluate(self, formatter, kwargs, mi, locals, html):
from qt.core import QDialog, QVBoxLayout from calibre.gui2.widgets2 import Dialog, HTMLDisplay class HTMLDialog(Dialog):

def __init__(self, title, prefs):
super().__init__(title, 'formatter_html_dialog', prefs=prefs)

def setup_ui(self):
l = QVBoxLayout(self)
d = self.display = HTMLDisplay()
l.addWidget(d)
l.addWidget(self.bb)

def set_html(self, tt_text):
self.display.setHtml(tt_text)

db = get_database(mi, 'show_dialog')
d = HTMLDialog(_('Template output'), db.new_api.backend.prefs)
d.set_html(html)
return '1' if d.exec() == QDialog.DialogCode.Accepted else ''

[문서]
class BuiltinFString(BuiltinFormatterFunction):
name = 'f_string'
arg_count = 1 category = FORMATTING_VALUES def __doc__getter__(self): return translate_ffml( r'''
``f_string(string)`` -- interpret ``string`` similar to how python interprets ``f`` strings.
The intended use is to simplify long sequences of ``str & str`` or strcat(a,b,c) expressions.

Text between braces (``{`` and ``}``) must be General Program Mode template expressions. The expressions, which can be expression lists, are evaluated in the current context (current book and local variables). Text not between braces is passed through unchanged.

Examples:
[LIST]
[*]``f_string('Here is the title: {$title}')`` - returns the string with ``{$title}`` replaced with the title of the current book. For example, if the book's title is
`20,000 Leagues Under the Sea` then the ``f_string()`` returns
`Here is the title: 20,000 Leagues Under the Sea`.
[*]Assuming the current date is 18 Sept 2025, this ``f_string()``
[CODE]
f_string("Today's date: the {d = today(); format_date(d, 'd')} of {format_date(d, 'MMMM')}, {format_date(d, 'yyyy')}")
[/CODE]
returns the string `Today's date: the 18 of September, 2025`.
Note the expression list (an assignment then an ``if`` statement) used in the first ``{ ... }`` group to assign today's date to a local variable.
[*]If the book is book #3 in a series named `Foo` that has 5 books then this template
[CODE]
program:
if $series then series_count = book_count('series:"""=' & $series & '"""', 0); return f_string("{$series}, book {$series_index} of {series_count}")
fi; return 'This book is not in a series'
[/CODE]
returns `Foo, book 3 of 5`
[/LIST]
''')

def evaluate(self, formatter, kwargs, mi, locals, fstring):
raise ValueError(_('This function cannot be called directly. It is built into the formatter'))

class FormatterUserFunction(FormatterFunction):

def __init__(self, name, doc, arg_count, program_text, object_type):
self.object_type = object_type self.name = name self.user_doc = doc self.arg_count = arg_count self._cached_program_text = program_text or ''
self.cached_compiled_text = None
# Keep this for external code compatibility. Set it to True if we have a
# python template function, otherwise false. This might break something
# if the code depends on stored templates being in GPM.
self.is_python = True if object_type is StoredObjectType.PythonFunction else False def to_pref(self):
return [self.name, self.doc, self.arg_count, self.program_text]

def __doc__getter__(self):
return self.user_doc tabs = re.compile(r'^\t*')

def function_object_type(thing):
# 'thing' can be a preference instance, program text, or an already-compiled function if isinstance(thing, FormatterUserFunction):
return thing.object_type if isinstance(thing, list):
text = thing[3]
else:
text = thing if text.startswith('def'):
return StoredObjectType.PythonFunction if text.startswith('program'):
return StoredObjectType.StoredGPMTemplate if text.startswith('python'):
return StoredObjectType.StoredPythonTemplate raise ValueError('Unknown program type in formatter function pref')

def function_pref_name(pref):
return pref[0]

def compile_user_function(name, doc, arg_count, eval_func):
typ = function_object_type(eval_func)
if typ is not StoredObjectType.PythonFunction:
return FormatterUserFunction(name, doc, arg_count, eval_func, typ)

def replace_func(mo):
return mo.group().replace('\t', ' ')

func = ' ' + '\n '.join([tabs.sub(replace_func, line)
for line in eval_func.splitlines()])
prog = '''
from calibre.utils.formatter_functions import FormatterUserFunction from calibre.utils.formatter_functions import formatter_functions class UserFunction(FormatterUserFunction):
''' + func locals_ = {}
if DEBUG and tweaks.get('enable_template_debug_printing', False):
print(prog)
exec(prog, locals_)
cls = locals_['UserFunction'](name, doc, arg_count, eval_func, typ)
return cls def compile_user_template_functions(funcs):
compiled_funcs = {}
for func in funcs:
try:
# Force a name conflict to test the logic
# if func[0] == 'myFunc2':
# func[0] = 'myFunc3'

# Compile the function so that the tab processing is done on the
# source. This helps ensure that if the function already is defined
# then white space differences don't cause them to compare differently cls = compile_user_function(*func)
cls.object_type = function_object_type(func)
compiled_funcs[cls.name] = cls except Exception:
try:
func_name = func[0]
except Exception:
func_name = 'Unknown'
prints(f'**** Compilation errors in user template function "{func_name}" ****')
traceback.print_exc(limit=10)
prints(f'**** End compilation errors in {func_name} "****"')
return compiled_funcs def load_user_template_functions(library_uuid, funcs, precompiled_user_functions=None):
unload_user_template_functions(library_uuid)
if precompiled_user_functions:
compiled_funcs = precompiled_user_functions else:
compiled_funcs = compile_user_template_functions(funcs)
formatter_functions().register_functions(library_uuid, list(compiled_funcs.values()))

def unload_user_template_functions(library_uuid):
formatter_functions().unregister_functions(library_uuid)

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

탐색

- 모듈
- 시작 »
- 모듈 코드 »
- calibre.web.feeds.news

calibre.web.feeds.news의 소스 코드

__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''
Defines various abstract base classes that can be subclassed to create powerful news fetching recipes.
'''
__docformat__ = 'restructuredtext en'

import io import os import re import sys import time import traceback from collections import defaultdict from contextlib import closing from urllib.parse import urlparse, urlsplit from calibre import __appname__, as_unicode, browser, force_unicode, iswindows, preferred_encoding, random_user_agent, strftime from calibre.ebooks.BeautifulSoup import BeautifulSoup, CData, NavigableString, Tag from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata.opf2 import OPFCreator from calibre.ebooks.metadata.toc import TOC from calibre.ptempfile import PersistentTemporaryFile from calibre.utils.date import now as nowf from calibre.utils.icu import numeric_sort_key from calibre.utils.img import add_borders_to_image, image_to_data, save_cover_data_to from calibre.utils.localization import _, canonicalize_lang, ngettext from calibre.utils.logging import ThreadSafeWrapper from calibre.utils.threadpool import NoResultsPending, ThreadPool, WorkRequest from calibre.web import Recipe from calibre.web.feeds import Feed, feed_from_xml, feeds_from_index, templates from calibre.web.fetch.simple import AbortArticle, RecursiveFetcher from calibre.web.fetch.simple import option_parser as web2disk_option_parser from calibre.web.fetch.utils import prepare_masthead_image def classes(classes):
q = frozenset(classes.split(' '))
return dict(attrs={'class': lambda x: x and frozenset(x.split()).intersection(q)})

def prefixed_classes(classes):
q = frozenset(classes.split(' '))

def matcher(x):
if x:
for candidate in frozenset(x.split()):
for x in q:
if candidate.startswith(x):
return True return False return {'attrs': {'class': matcher}}

class LoginFailed(ValueError):
pass class DownloadDenied(ValueError):
pass

[문서]
class BasicNewsRecipe(Recipe):
'''
Base class that contains logic needed in all recipes. By overriding progressively more of the functionality in this class, you can make progressively more customized/powerful recipes. For a tutorial introduction to creating recipes, see :doc:`news`.
'''

#: The title to use for the e-book title = _('Unknown News Source')

#: A couple of lines that describe the content this recipe downloads.
#: This will be used primarily in a GUI that presents a list of recipes.
description = ''

#: The author of this recipe
__author__ = __appname__

#: Minimum calibre version needed to use this recipe requires_version = (0, 6, 0)

#: The language that the news is in. Must be an ISO-639 code either
#: two or three characters long language = 'und'

#: Maximum number of articles to download from each feed. This is primarily
#: useful for feeds that don't have article dates. For most feeds, you should
#: use :attr:`BasicNewsRecipe.oldest_article` max_articles_per_feed = 100

#: Oldest article to download from this news source. In days.
oldest_article = 7.0

#: Number of levels of links to follow on article webpages recursions = 0

#: The default delay between consecutive downloads in seconds. The argument may be a
#: floating point number to indicate a more precise time. See :meth:`get_url_specific_delay`
#: to implement per URL delays.
delay = 0

#: Publication type
#: Set to newspaper, magazine or blog. If set to None, no publication type
#: metadata will be written to the opf file.
publication_type = 'unknown'

#: Number of simultaneous downloads. Set to 1 if the server is picky.
#: Automatically reduced to 1 if :attr:`BasicNewsRecipe.delay` > 0 simultaneous_downloads = 5

#: Timeout for fetching files from server in seconds timeout = 120.0

#: The format string for the date shown on the first page.
#: By default: Day_Name, Day_Number Month_Name Year timefmt = ' [%a, %d %b %Y]'

#: List of feeds to download.
#: Can be either ``[url1, url2, ...]`` or ``[('title1', url1), ('title2', url2),...]`` feeds = None

#: Max number of characters in the short description summary_length = 500

#: Convenient flag to disable loading of stylesheets for websites
#: that have overly complex stylesheets unsuitable for conversion
#: to e-book formats.
#: If True stylesheets are not downloaded and processed no_stylesheets = False

#: Convenient flag to strip all JavaScript tags from the downloaded HTML remove_javascript = True

#: If True the GUI will ask the user for a username and password
#: to use while downloading.
#: If set to "optional" the use of a username and password becomes optional needs_subscription = False

#: If True the navigation bar is center aligned, otherwise it is left aligned center_navbar = True

#: Specify an override encoding for sites that have an incorrect
#: charset specification. The most common being specifying ``latin1`` and
#: using ``cp1252``. If None, try to detect the encoding. If it is a
#: callable, the callable is called with two arguments: The recipe object
#: and the source to be decoded. It must return the decoded source.
encoding = None

#: Normally we try to guess if a feed has full articles embedded in it
#: based on the length of the embedded content. If `None`, then the
#: default guessing is used. If `True` then the we always assume the feeds has
#: embedded content and if `False` we always assume the feed does not have
#: embedded content.
use_embedded_content = None

#: Set to True and implement :meth:`get_obfuscated_article` to handle
#: websites that try to make it difficult to scrape content.
articles_are_obfuscated = False

#: Reverse the order of articles in each feed reverse_article_order = False

#: Automatically extract all the text from downloaded article pages. Uses
#: the algorithms from the readability project. Setting this to True, means
#: that you do not have to worry about cleaning up the downloaded HTML
#: manually (though manual cleanup will always be superior).
auto_cleanup = False

#: Specify elements that the auto cleanup algorithm should never remove.
#: The syntax is a XPath expression. For example::
#:
#: auto_cleanup_keep = '//div[@id="article-image"]' will keep all divs with
#: id="article-image"
#: auto_cleanup_keep = '//*[@class="important"]' will keep all elements
#: with class="important"
#: auto_cleanup_keep = '//div[@id="article-image"]|//span[@class="important"]'
#: will keep all divs with id="article-image" and spans
#: with class="important"
#:
auto_cleanup_keep = None

#: Specify any extra :term:`CSS` that should be added to downloaded :term:`HTML` files.
#: It will be inserted into `<style>` tags, just before the closing
#: `</head>` tag thereby overriding all :term:`CSS` except that which is
#: declared using the style attribute on individual :term:`HTML` tags.
#: Note that if you want to programmatically generate the extra_css override
#: the :meth:`get_extra_css()` method instead.
#: For example::
#:
#: extra_css = '.heading { font: serif x-large }'
#:
extra_css = None

#: If True empty feeds are removed from the output.
#: This option has no effect if parse_index is overridden in
#: the sub class. It is meant only for recipes that return a list
#: of feeds using `feeds` or :meth:`get_feeds`. It is also used if you use
#: the ignore_duplicate_articles option.
remove_empty_feeds = False

#: List of regular expressions that determines which links to follow.
#: If empty, it is ignored. Used only if is_link_wanted is
#: not implemented. For example::
#:
#: match_regexps = [r'page=[0-9]+']
#:
#: will match all URLs that have `page=some number` in them.
#:
#: Only one of :attr:`BasicNewsRecipe.match_regexps` or
#: :attr:`BasicNewsRecipe.filter_regexps` should be defined.
match_regexps = []

#: List of regular expressions that determines which links to ignore.
#: If empty it is ignored. Used only if is_link_wanted is not
#: implemented. For example::
#:
#: filter_regexps = [r'ads\.doubleclick\.net']
#:
#: will remove all URLs that have `ads.doubleclick.net` in them.
#:
#: Only one of :attr:`BasicNewsRecipe.match_regexps` or
#: :attr:`BasicNewsRecipe.filter_regexps` should be defined.
filter_regexps = []

#: Recipe specific options to control the conversion of the downloaded
#: content into an e-book. These will override any user or plugin specified
#: values, so only use if absolutely necessary. For example::
#:
#: conversion_options = {
#: 'base_font_size' : 16,
#: 'linearize_tables' : True,
#: }
#:
conversion_options = {}

#: List of tags to be removed. Specified tags are removed from downloaded HTML.
#: A tag is specified as a dictionary of the form::
#:
#: {
#: name : 'tag name', #e.g. 'div'
#: attrs : a dictionary, #e.g. {'class': 'advertisement'}
#: }
#:
#: All keys are optional. For a full explanation of the search criteria, see
#: `Beautiful Soup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/#searching-the-tree>`__
#: A common example::
#:
#: remove_tags = [dict(name='div', class_='advert')]
#:
#: This will remove all `<div class="advert">` tags and all
#: their children from the downloaded :term:`HTML`.
remove_tags = []

#: Remove all tags that occur after the specified tag.
#: For the format for specifying a tag see :attr:`BasicNewsRecipe.remove_tags`.
#: For example::
#:
#: remove_tags_after = [dict(id='content')]
#:
#: will remove all
#: tags after the first element with `id="content"`.
remove_tags_after = None

#: Remove all tags that occur before the specified tag.
#: For the format for specifying a tag see :attr:`BasicNewsRecipe.remove_tags`.
#: For example::
#:
#: remove_tags_before = dict(id='content')
#:
#: will remove all
#: tags before the first element with `id="content"`.
remove_tags_before = None

#: List of attributes to remove from all tags.
#: For example::
#:
#: remove_attributes = ['style', 'font']
remove_attributes = []

#: Keep only the specified tags and their children.
#: For the format for specifying a tag see :attr:`BasicNewsRecipe.remove_tags`.
#: If this list is not empty, then the `<body>` tag will be emptied and re-filled with
#: the tags that match the entries in this list. For example::
#:
#: keep_only_tags = [dict(id=['content', 'heading'])]
#:
#: will keep only tags that have an `id` attribute of `"content"` or `"heading"`.
keep_only_tags = []

#: List of :term:`regexp` substitution rules to run on the downloaded :term:`HTML`.
#: Each element of the
#: list should be a two element tuple. The first element of the tuple should
#: be a compiled regular expression and the second a callable that takes
#: a single match object and returns a string to replace the match. For example::
#:
#: preprocess_regexps = [
#: (re.compile(r'<!--Article ends here-->.*</body>', re.DOTALL|re.IGNORECASE),
#: lambda match: '</body>'),
#: ]
#:
#: will remove everything from `<!--Article ends here-->` to `</body>`.
preprocess_regexps = []

#: The CSS that is used to style the templates, i.e., the navigation bars and
#: the Tables of Contents. Rather than overriding this variable, you should
#: use `extra_css` in your recipe to customize look and feel.
template_css = '''
.article_date { color: gray; font-family: monospace;
}

.article_description { text-indent: 0pt;
}

a.article { font-weight: bold; text-align:left;
}

a.feed { font-weight: bold;
}

.calibre_navbar { font-family:monospace;
}
'''

#: By default, calibre will use a default image for the masthead (Kindle only).
#: Override this in your recipe to provide a URL to use as a masthead.
masthead_url = None

#: By default, the cover image returned by get_cover_url() will be used as
#: the cover for the periodical. Overriding this in your recipe instructs
#: calibre to render the downloaded cover into a frame whose width and height
#: are expressed as a percentage of the downloaded cover.
#: cover_margins = (10, 15, '#ffffff') pads the cover with a white margin
#: 10px on the left and right, 15px on the top and bottom.
#: Color names are defined `here <https://www.imagemagick.org/script/color.php>`_.
#: Note that for some reason, white does not always work in Windows. Use
#: #ffffff instead cover_margins = (0, 0, '#ffffff')

#: Set to a non empty string to disable this recipe.
#: The string will be used as the disabled message recipe_disabled = None

#: Ignore duplicates of articles that are present in more than one section.
#: A duplicate article is an article that has the same title and/or URL.
#: To ignore articles with the same title, set this to::
#:
#: ignore_duplicate_articles = {'title'}
#:
#: To use URLs instead, set it to::
#:
#: ignore_duplicate_articles = {'url'}
#:
#: To match on title or URL, set it to::
#:
#: ignore_duplicate_articles = {'title', 'url'}
ignore_duplicate_articles = None

# The following parameters control how the recipe attempts to minimize
# image sizes. Note that if compression is enabled PNG images are converted
# to JPEG.

#: Set this to False to ignore all scaling and compression parameters and
#: pass images through unmodified. If True and the other compression
#: parameters are left at their default values, images will be scaled to fit
#: in the screen dimensions set by the output profile and compressed to size at
#: most (w * h)/16 where w x h are the scaled image dimensions.
compress_news_images = False

#: The factor used when auto compressing JPEG images. If set to None,
#: auto compression is disabled. Otherwise, the images will be reduced in size to
#: (w * h)/compress_news_images_auto_size bytes if possible by reducing
#: the quality level, where w x h are the image dimensions in pixels.
#: The minimum JPEG quality will be 5/100 so it is possible this constraint
#: will not be met. This parameter can be overridden by the parameter
#: compress_news_images_max_size which provides a fixed maximum size for images.
#: Note that if you enable scale_news_images_to_device then the image will
#: first be scaled and then its quality lowered until its size is less than
#: (w * h)/factor where w and h are now the *scaled* image dimensions. In
#: other words, this compression happens after scaling.
compress_news_images_auto_size = 16

#: Set JPEG quality so images do not exceed the size given (in KBytes).
#: If set, this parameter overrides auto compression via compress_news_images_auto_size.
#: The minimum JPEG quality will be 5/100 so it is possible this constraint
#: will not be met.
compress_news_images_max_size = None

#: Rescale images to fit in the device screen dimensions set by the output profile.
#: Ignored if no output profile is set.
scale_news_images_to_device = True

#: Maximum dimensions (w,h) to scale images to. If scale_news_images_to_device is True
#: this is set to the device screen dimensions set by the output profile unless
#: there is no profile set, in which case it is left at whatever value it has been
#: assigned (default None).
scale_news_images = None

#: If set to True then links in downloaded articles that point to other downloaded articles are
#: changed to point to the downloaded copy of the article rather than its original web URL. If you
#: set this to True, you might also need to implement :meth:`canonicalize_internal_url` to work
#: with the URL scheme of your particular website.
resolve_internal_links = False

#: Specify options specific to this recipe. These will be available for the user to customize
#: in the Advanced tab of the Fetch News dialog or at the ebook-convert command line. The options
#: are specified as a dictionary mapping option name to metadata about the option. For example::
#:
#: recipe_specific_options = {
#: 'edition_date': {
#: 'short': 'The issue date to download',
#: 'long': 'Specify a date in the format YYYY-mm-dd to download the issue corresponding to that date',
#: 'default': 'current',
#: }
#: }
#:
#: When the recipe is run, self.recipe_specific_options will be a dict mapping option name to the option value
#: specified by the user. When the option is unspecified by the user, it will have the value specified by 'default'.
#: If no default is specified, the option will not be in the dict at all, when unspecified by the user.
recipe_specific_options = None

#: The simulated browser engine to use when downloading from servers. The default is to use the Python mechanize
#: browser engine, which supports logging in. However, if you don't need logging in, consider changing this
#: to either 'webengine' which uses an actual Chromium browser to do the network requests or 'qt' which
#: uses the Qt Networking backend. Both 'webengine' and 'qt' support HTTP/2, which mechanize does not and
#: are thus harder to fingerprint for bot protection services.
browser_type = 'mechanize'

#: Set to False if you do not want to use gzipped transfers with the mechanize browser.
#: Note that some old servers flake out with gzip.
handle_gzip = True

# See the built-in recipes for examples of these settings.

def short_title(self):
return force_unicode(self.title, preferred_encoding)

[문서]
def is_link_wanted(self, url, tag):
'''
Return True if the link should be followed or False otherwise. By default, raises NotImplementedError which causes the downloader to ignore it.

:param url: The URL to be followed
:param tag: The tag from which the URL was derived
'''
raise NotImplementedError()

[문서]
def get_extra_css(self):
'''
By default returns `self.extra_css`. Override if you want to programmatically generate the extra_css.
'''
return self.extra_css

[문서]
def get_cover_url(self):
'''
Return a :term:`URL` to the cover image for this issue or `None`.
By default it returns the value of the member `self.cover_url` which is normally `None`. If you want your recipe to download a cover for the e-book override this method in your subclass, or set the member variable `self.cover_url` before this method is called.
'''
return getattr(self, 'cover_url', None)

[문서]
def get_masthead_url(self):
'''
Return a :term:`URL` to the masthead image for this issue or `None`.
By default it returns the value of the member `self.masthead_url` which is normally `None`. If you want your recipe to download a masthead for the e-book override this method in your subclass, or set the member variable `self.masthead_url` before this method is called.
Masthead images are used in Kindle MOBI files.
'''
return getattr(self, 'masthead_url', None)

[문서]
def get_feeds(self):
'''
Return a list of :term:`RSS` feeds to fetch for this profile. Each element of the list must be a 2-element tuple of the form (title, url). If title is None or an empty string, the title from the feed is used. This method is useful if your recipe needs to do some processing to figure out the list of feeds to download. If so, override in your subclass.
'''
if not self.feeds:
raise NotImplementedError()
if self.test:
return self.feeds[:self.test[0]]
return self.feeds

[문서]
def get_url_specific_delay(self, url):
'''
Return the delay in seconds before downloading this URL. If you want to programmatically determine the delay for the specified URL, override this method in your subclass, returning self.delay by default for URLs you do not want to affect.

:return: A floating point number, the delay in seconds.
'''
return self.delay

[문서]
@classmethod def print_version(cls, url):
'''
Take a `url` pointing to the webpage with article content and return the
:term:`URL` pointing to the print version of the article. By default does nothing. For example::

def print_version(self, url):
return url + '?&pagewanted=print'

'''
raise NotImplementedError()

[문서]
@classmethod def image_url_processor(cls, baseurl, url):
'''
Perform some processing on image urls (perhaps removing size restrictions for dynamically generated images, etc.) and return the processed URL. Return None or an empty string to skip fetching the image.
'''
return url

[문서]
def preprocess_image(self, img_data, image_url):
'''
Perform some processing on downloaded image data. This is called on the raw data before any resizing is done. Must return the processed raw data. Return
None to skip the image.
'''
return img_data

[문서]
def get_browser(self, *args, **kwargs):
'''
Return a browser instance used to fetch documents from the web. By default it returns a `mechanize <https://mechanize.readthedocs.io/en/latest/>`_ browser instance that supports cookies, ignores robots.txt, handles refreshes and has a random common user agent.

To customize the browser override this method in your sub-class as::

def get_browser(self, *a, **kw):
br = super().get_browser(*a, **kw)
# Add some headers br.addheaders += [
('My-Header', 'one'),
('My-Header2', 'two'),
]
# Set some cookies br.set_cookie('name', 'value')
br.set_cookie('name2', 'value2', domain='.mydomain.com')
# Make a POST request with some data br.open('https://someurl.com', {'username': 'def', 'password': 'pwd'}).read()
# Do a login via a simple web form (only supported with mechanize browsers)
if self.username is not None and self.password is not None:
br.open('https://www.nytimes.com/auth/login')
br.select_form(name='login')
br['USERID'] = self.username br['PASSWORD'] = self.password br.submit()
return br

'''
if 'user_agent' not in kwargs:
# More and more news sites are serving JPEG XR images to IE ua = getattr(self, 'last_used_user_agent', None) or self.calibre_most_common_ua or random_user_agent(allow_ie=False)
kwargs['user_agent'] = self.last_used_user_agent = ua self.log('Using user agent:', kwargs['user_agent'])
if self.browser_type != 'mechanize':
from calibre.scraper.qt import Browser, WebEngineBrowser return {'qt': Browser, 'webengine': WebEngineBrowser}[self.browser_type]( user_agent=kwargs['user_agent'], verify_ssl_certificates=kwargs.get('verify_ssl_certificates', False))
br = browser(*args, **kwargs)
br.addheaders += [('Accept', '*/*')]
if self.handle_gzip:
br.set_handle_gzip(True)
return br

[문서]
def clone_browser(self, br):
'''
Clone the browser br. Cloned browsers are used for multi-threaded downloads, since mechanize is not thread safe. The default cloning routines should capture most browser customization, but if you do something exotic in your recipe, you should override this method in your recipe and clone manually.

Cloned browser instances use the same, thread-safe CookieJar by default, unless you have customized cookie handling.
'''
if callable(getattr(br, 'clone_browser', None)):
return br.clone_browser()

# Uh-oh recipe using something exotic, call get_browser return self.get_browser()

@property def cloned_browser(self):
if hasattr(self.get_browser, 'is_base_class_implementation') and self.browser_type == 'mechanize':
# We are using the default get_browser, which means no need to
# clone br = BasicNewsRecipe.get_browser(self)
else:
br = self.clone_browser(self.browser)
return br

[문서]
def get_article_url(self, article):
'''
Override in a subclass to customize extraction of the :term:`URL` that points to the content for each article. Return the article URL. It is called with `article`, an object representing a parsed article from a feed. See `feedparser <https://pythonhosted.org/feedparser/>`_.
By default it looks for the original link (for feeds syndicated via a service like FeedBurner or Pheedo) and if found, returns that or else returns
`article.link <https://pythonhosted.org/feedparser/reference-entry-link.html>`_.
'''
for key in article.keys():
if key.endswith('_origlink'):
url = article[key]
if url and (url.startswith(('http://', 'https://'))):
return url ans = article.get('link', None)
if not ans and getattr(article, 'links', None):
for item in article.links:
if item.get('rel', 'alternate') == 'alternate':
ans = item['href']
break return ans

[문서]
def skip_ad_pages(self, soup):
'''
This method is called with the source of each downloaded :term:`HTML` file, before any of the cleanup attributes like remove_tags, keep_only_tags are applied. Note that preprocess_regexps will have already been applied.
It is meant to allow the recipe to skip ad pages. If the soup represents an ad page, return the HTML of the real page. Otherwise return
None.

`soup`: A `BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/>`__ instance containing the downloaded :term:`HTML`.
'''
return

[문서]
def abort_article(self, msg=None):
''' Call this method inside any of the preprocess methods to abort the download for the current article. Useful to skip articles that contain inappropriate content, such as pure video articles. '''
raise AbortArticle(msg or _('Article download aborted'))

[문서]
def preprocess_raw_html(self, raw_html, url):
'''
This method is called with the source of each downloaded :term:`HTML` file, before it is parsed into an object tree. raw_html is a unicode string representing the raw HTML downloaded from the web. url is the URL from which the HTML was downloaded.

Note that this method acts *before* preprocess_regexps.

This method must return the processed raw_html as a unicode object.
'''
return raw_html
def preprocess_raw_html_(self, raw_html, url):
raw_html = self.preprocess_raw_html(raw_html, url)
if self.auto_cleanup:
try:
raw_html = self.extract_readable_article(raw_html, url)
except Exception:
self.log.exception(f'Auto cleanup of URL: {url!r} failed')

return raw_html

[문서]
def preprocess_html(self, soup):
'''
This method is called with the source of each downloaded :term:`HTML` file, before it is parsed for links and images. It is called after the cleanup as specified by remove_tags etc.
It can be used to do arbitrarily powerful pre-processing on the :term:`HTML`.
It should return `soup` after processing it.

`soup`: A `BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/>`__ instance containing the downloaded :term:`HTML`.
'''
return soup

[문서]
def postprocess_html(self, soup, first_fetch):
'''
This method is called with the source of each downloaded :term:`HTML` file, after it is parsed for links and images.
It can be used to do arbitrarily powerful post-processing on the :term:`HTML`.
It should return `soup` after processing it.

:param soup: A `BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/>`__ instance containing the downloaded :term:`HTML`.
:param first_fetch: True if this is the first page of an article.

'''
return soup

[문서]
def cleanup(self):
'''
Called after all articles have been download. Use it to do any cleanup like logging out of subscription sites, etc.
'''
pass

[문서]
def canonicalize_internal_url(self, url, is_link=True):
'''
Return a set of canonical representations of ``url``. The default implementation uses just the server hostname and path of the URL, ignoring any query parameters, fragments, etc. The canonical representations must be unique across all URLs for this news source. If they are not, then internal links may be resolved incorrectly.

:param is_link: Is True if the URL is coming from an internal link in an HTML file. False if the URL is the URL used to download an article.
'''
try:
parts = urlparse(url)
except Exception:
self.log.error(f'Failed to parse url: {url!r}, ignoring')
return frozenset()
nl = parts.netloc path = parts.path or ''
if isinstance(nl, bytes):
nl = nl.decode('utf-8', 'replace')
if isinstance(path, bytes):
path = path.decode('utf-8', 'replace')
return frozenset({(nl, path.rstrip('/'))})

[문서]
def index_to_soup(self, url_or_raw, raw=False, as_tree=False, save_raw=None):
'''
Convenience method that takes an URL to the index page and returns a `BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/bs4/doc>`__ of it.

`url_or_raw`: Either a URL or the downloaded index page as a string
'''
if re.match((br'\w+://' if isinstance(url_or_raw, bytes) else r'\w+://'), url_or_raw):
# We may be called in a thread (in the skip_ad_pages method), so
# clone the browser to be safe. We cannot use self.cloned_browser
# as it may or may not actually clone the browser, depending on if
# the recipe implements get_browser() or not br = self.clone_browser(self.browser)
open_func = getattr(br, 'open_novisit', br.open)
with closing(open_func(url_or_raw, timeout=self.timeout)) as f:
_raw = f.read()
if not _raw:
raise RuntimeError(f'Could not fetch index from {url_or_raw}')
else:
_raw = url_or_raw if raw:
return _raw if not isinstance(_raw, str) and self.encoding:
if callable(self.encoding):
_raw = self.encoding(_raw)
else:
_raw = _raw.decode(self.encoding, 'replace')
from calibre.ebooks.chardet import strip_encoding_declarations, xml_to_unicode from calibre.utils.cleantext import clean_xml_chars if isinstance(_raw, str):
_raw = strip_encoding_declarations(_raw)
else:
_raw = xml_to_unicode(_raw, strip_encoding_pats=True, resolve_entities=True)[0]
_raw = clean_xml_chars(_raw)
if save_raw:
with open(save_raw, 'wb') as f:
f.write(_raw.encode('utf-8'))
if as_tree:
from html5_parser import parse return parse(_raw)
return BeautifulSoup(_raw)[문서]
def extract_readable_article(self, html, url):
'''
Extracts main article content from 'html', cleans up and returns as a (article_html, extracted_title) tuple.
Based on the original readability algorithm by Arc90.
'''
from lxml.html import tostring from calibre.ebooks.readability import readability from calibre.utils.xml_parse import document_fromstring, fragment_fromstring doc = readability.Document(html, self.log, url=url, keep_elements=self.auto_cleanup_keep)
article_html = doc.summary()
extracted_title = doc.title()

try:
frag = fragment_fromstring(article_html)
except Exception:
doc = document_fromstring(article_html)
frag = doc.xpath('//body')[-1]
if frag.tag == 'html':
root = frag elif frag.tag == 'body':
root = document_fromstring( f'<html><head><title>{extracted_title}</title></head></html>')
root.append(frag)
else:
root = document_fromstring( f'<html><head><title>{extracted_title}</title></head><body/></html>')
root.xpath('//body')[0].append(frag)

body = root.xpath('//body')[0]
has_title = False for x in body.iterdescendants():
if x.text == extracted_title:
has_title = True inline_titles = body.xpath('//h1|//h2')
if not has_title and not inline_titles:
heading = body.makeelement('h2')
heading.text = extracted_title body.insert(0, heading)

raw_html = tostring(root, encoding='unicode')

return raw_html

[문서]
def sort_index_by(self, index, weights):
'''
Convenience method to sort the titles in `index` according to `weights`.
`index` is sorted in place. Returns `index`.

`index`: A list of titles.

`weights`: A dictionary that maps weights to titles. If any titles in index are not in weights, they are assumed to have a weight of 0.
'''
weights = defaultdict(int, weights)
index.sort(key=lambda x: weights[x])
return index

[문서]
def parse_index(self):
'''
This method should be implemented in recipes that parse a website instead of feeds to generate a list of articles. Typical uses are for news sources that have a "Print Edition" webpage that lists all the articles in the current print edition. If this function is implemented, it will be used in preference to :meth:`BasicNewsRecipe.parse_feeds`.

It must return a list. Each element of the list must be a 2-element tuple of the form ``('feed title', list of articles)``.

Each list of articles must contain dictionaries of the form::

{
'title' : article title,
'url' : URL of print version,
'date' : The publication date of the article as a string,
'description' : A summary of the article
'content' : The full article (can be an empty string). Obsolete do not use, instead save the content to a temporary file and pass a file:///path/to/temp/file.html as the URL.
}

For an example, see the recipe for downloading `The Atlantic`.
In addition, you can add 'author' for the author of the article.

If you want to abort processing for some reason and have calibre show the user a simple message instead of an error, call
:meth:`abort_recipe_processing`.
'''
raise NotImplementedError()

[문서]
def abort_recipe_processing(self, msg):
'''
Causes the recipe download system to abort the download of this recipe, displaying a simple feedback message to the user.
'''
from calibre.ebooks.conversion import ConversionUserFeedBack raise ConversionUserFeedBack(_('Failed to download %s')%self.title, msg)

[문서]
def get_obfuscated_article(self, url):
'''
If you set `articles_are_obfuscated` this method is called with every article URL. It should return the path to a file on the filesystem that contains the article HTML. That file is processed by the recursive
HTML fetching engine, so it can contain links to pages/images on the web.
Alternately, you can return a dictionary of the form:
{'data': <HTML data>, 'url': <the resolved URL of the article>}. This avoids needing to create temporary files. The `url` key in the dictionary is useful if the effective URL of the article is different from the URL passed into this method, for example, because of redirects. It can be omitted if the URL is unchanged.

This method is typically useful for sites that try to make it difficult to access article content automatically.
'''
raise NotImplementedError()

[문서]
def add_toc_thumbnail(self, article, src):
'''
Call this from populate_article_metadata with the src attribute of an
<img> tag from the article that is appropriate for use as the thumbnail representing the article in the Table of Contents. Whether the thumbnail is actually used is device dependent (currently only used by the Kindles). Note that the referenced image must be one that was successfully downloaded, otherwise it will be ignored.
'''
if not src or not hasattr(article, 'toc_thumbnail'):
return src = src.replace('\\', '/')
if re.search(r'feed_\d+/article_\d+/images/img', src, flags=re.I) is None:
self.log.warn(f'Ignoring invalid TOC thumbnail image: {src!r}')
return article.toc_thumbnail = re.sub(r'^.*?feed', 'feed', src, flags=re.IGNORECASE)

[문서]
def populate_article_metadata(self, article, soup, first):
'''
Called when each HTML page belonging to article is downloaded.
Intended to be used to get article metadata like author/summary/etc.
from the parsed HTML (soup).

:param article: A object of class :class:`calibre.web.feeds.Article`.
If you change the summary, remember to also change the text_summary
:param soup: Parsed HTML belonging to this article
:param first: True iff the parsed HTML is the first page of the article.
'''
pass

[문서]
def postprocess_book(self, oeb, opts, log):
'''
Run any needed post processing on the parsed downloaded e-book.

:param oeb: An OEBBook object
:param opts: Conversion options
'''
pass
def __init__(self, options, log, progress_reporter):
'''
Initialize the recipe.
:param options: Parsed commandline options
:param log: Logging object
:param progress_reporter: A Callable that takes two arguments: progress (a number between 0 and 1) and a string message. The message should be optional.
'''
self.log = ThreadSafeWrapper(log)
if not isinstance(self.title, str):
self.title = str(self.title, 'utf-8', 'replace')

self.debug = options.verbose > 1 self.output_dir = os.path.abspath(os.getcwd())
self.verbose = options.verbose self.test = options.test if self.test and not isinstance(self.test, tuple):
self.test = (2, 2)
self.username = options.username self.password = options.password self.lrf = options.lrf self.output_profile = options.output_profile self.touchscreen = getattr(self.output_profile, 'touchscreen', False)
if self.touchscreen:
self.template_css += self.output_profile.touchscreen_news_css if self.test:
self.max_articles_per_feed = self.test[1]
self.simultaneous_downloads = min(4, self.simultaneous_downloads)

if self.debug:
self.verbose = True self.report_progress = progress_reporter if self.needs_subscription and ( self.username is None or self.password is None or ( not self.username and not self.password)):
if self.needs_subscription != 'optional':
raise ValueError(_('The "%s" recipe needs a username and password.')%self.title)

self.browser = self.get_browser()
self.image_map, self.image_counter = {}, 1 self.css_map = {}

web2disk_cmdline = ['web2disk',
'--timeout', str(self.timeout),
'--max-recursions', str(self.recursions),
'--delay', str(self.delay),
]

if self.verbose:
web2disk_cmdline.append('--verbose')

if self.no_stylesheets:
web2disk_cmdline.append('--dont-download-stylesheets')

for reg in self.match_regexps:
web2disk_cmdline.extend(['--match-regexp', reg])

for reg in self.filter_regexps:
web2disk_cmdline.extend(['--filter-regexp', reg])

if options.output_profile.short_name in ('default', 'tablet'):
self.scale_news_images_to_device = False elif self.scale_news_images_to_device:
self.scale_news_images = options.output_profile.screen_size self.web2disk_options = web2disk_option_parser().parse_args(web2disk_cmdline)[0]
for extra in ('keep_only_tags', 'remove_tags', 'preprocess_regexps',
'skip_ad_pages', 'preprocess_html', 'remove_tags_after',
'remove_tags_before', 'is_link_wanted',
'compress_news_images', 'compress_news_images_max_size',
'compress_news_images_auto_size', 'scale_news_images'):
setattr(self.web2disk_options, extra, getattr(self, extra))

self.web2disk_options.postprocess_html = self._postprocess_html self.web2disk_options.preprocess_image = self.preprocess_image self.web2disk_options.encoding = self.encoding self.web2disk_options.preprocess_raw_html = self.preprocess_raw_html_ self.web2disk_options.get_delay = self.get_url_specific_delay if self.delay > 0:
self.simultaneous_downloads = 1 self.navbar = templates.TouchscreenNavBarTemplate() if self.touchscreen else \ templates.NavBarTemplate()
self.failed_downloads = []
self.partial_failures = []
self.aborted_articles = []
self.recipe_specific_options_metadata = rso = self.recipe_specific_options or {}
self.recipe_specific_options = {k: rso[k]['default'] for k in rso if 'default' in rso[k]}
for x in (options.recipe_specific_option or ()):
k, sep, v = x.partition(':')
if not sep:
raise ValueError(f'{x} is not a valid recipe specific option')
if k not in rso:
raise KeyError(f'{k} is not an option supported by: {self.title}')
self.recipe_specific_options[k] = v if self.recipe_specific_options:
log('Recipe specific options:')
for k, v in self.recipe_specific_options.items():
log(' ', f'{k} = {v}')

def _postprocess_html(self, soup, first_fetch, job_info):
if self.no_stylesheets:
for link in soup.findAll('link'):
if (link.get('type') or 'text/css').lower() == 'text/css' and 'stylesheet' in (link.get('rel') or ('stylesheet',)):
link.extract()
for style in soup.findAll('style'):
style.extract()
head = soup.find('head')
if not head:
head = soup.find('body')
if not head:
head = soup.find(True)
css = self.template_css + '\n\n' + (self.get_extra_css() or '')
style = soup.new_tag('style', type='text/css', title='override_css')
style.append(css)
head.append(style)
if first_fetch and job_info:
url, f, a, feed_len = job_info body = soup.find('body')
if body is not None:
templ = self.navbar.generate(False, f, a, feed_len, not self.has_single_feed, url, __appname__, center=self.center_navbar, extra_css=self.get_extra_css() or '')
elem = BeautifulSoup(templ.render(doctype='xhtml').decode('utf-8')).find('div')
body.insert(0, elem)
# This is needed because otherwise inserting elements into
# the soup breaks find()
soup = BeautifulSoup(soup.decode_contents())
if self.remove_javascript:
for script in list(soup.findAll('script')):
script.extract()
for o in soup.findAll(onload=True):
del o['onload']

for attr in self.remove_attributes:
for x in soup.findAll(attrs={attr:True}):
del x[attr]
for bad_tag in list(soup.findAll(['base', 'iframe', 'canvas', 'embed', 'button',
'command', 'datalist', 'video', 'audio', 'noscript', 'link', 'meta'])):
# link tags can be used for preloading causing network activity in
# calibre viewer. meta tags can do all sorts of crazy things,
# including http-equiv refresh, viewport shenanigans, etc.
bad_tag.extract()
# srcset causes some viewers, like calibre's to load images from the
# web, and it also possible causes iBooks on iOS to barf, see
# https://bugs.launchpad.net/bugs/1713986 for img in soup.findAll('img', srcset=True):
del img['srcset']

ans = self.postprocess_html(soup, first_fetch)

# Nuke HTML5 tags for x in ans.findAll(['article', 'aside', 'header', 'footer', 'nav',
'figcaption', 'figure', 'section']):
if x.get('class'):
x.get_attribute_list('class').append(f'calibre-nuked-tag-{x.name}')
else:
x['class'] = f'calibre-nuked-tag-{x.name}'
x.name = 'div'

if job_info:
url, f, a, feed_len = job_info try:
article = self.feed_objects[f].articles[a]
except Exception:
self.log.exception('Failed to get article object for postprocessing')
else:
self.populate_article_metadata(article, ans, first_fetch)
return ans

[문서]
def download(self):
'''
Download and pre-process all articles from the feeds in this recipe.
This method should be called only once on a particular Recipe instance.
Calling it more than once will lead to undefined behavior.
:return: Path to index.html
'''
try:
res = self.build_index()
self.report_progress(1, _('Download finished'))
if self.failed_downloads:
self.log.warning(_('Failed to download the following articles:'))
for feed, article, debug in self.failed_downloads:
self.log.warning(article.title, 'from', feed.title)
self.log.debug(article.url)
self.log.debug(debug)
if self.partial_failures:
self.log.warning(_('Failed to download parts of the following articles:'))
for feed, atitle, aurl, debug in self.partial_failures:
self.log.warning(atitle + _(' from ') + feed)
self.log.debug(aurl)
self.log.warning(_('\tFailed links:'))
for l, tb in debug:
self.log.warning(l)
self.log.debug(tb)
return res finally:
self.cleanup()@property def lang_for_html(self):
try:
lang = self.language.replace('_', '-').partition('-')[0].lower()
if lang == 'und':
lang = None except Exception:
lang = None return lang def feeds2index(self, feeds):
templ = (templates.TouchscreenIndexTemplate if self.touchscreen else templates.IndexTemplate)
templ = templ(lang=self.lang_for_html)
css = self.template_css + '\n\n' +(self.get_extra_css() or '')
timefmt = self.timefmt return templ.generate(self.title, 'mastheadImage.jpg', timefmt, feeds, extra_css=css).render(doctype='xhtml')

@classmethod def description_limiter(cls, src):
if not src:
return ''
src = force_unicode(src, 'utf-8')
pos = cls.summary_length fuzz = 50 si = src.find(';', pos)
if si > 0 and si-pos > fuzz:
si = -1 gi = src.find('>', pos)
if gi > 0 and gi-pos > fuzz:
gi = -1 npos = max(si, gi)
if npos < 0:
npos = pos ans = src[:npos+1]
if len(ans) < len(src):
from calibre.utils.cleantext import clean_xml_chars

# Truncating the string could cause a dangling UTF-16 half-surrogate, which will cause lxml to barf, clean it ans = clean_xml_chars(ans) + '…'
return ans def feed2index(self, f, feeds):
feed = feeds[f]
if feed.image_url is not None: # Download feed image imgdir = os.path.join(self.output_dir, 'images')
if not os.path.isdir(imgdir):
os.makedirs(imgdir)

if feed.image_url in self.image_map:
feed.image_url = self.image_map[feed.image_url]
else:
bn = urlsplit(feed.image_url).path if bn:
bn = bn.rpartition('/')[-1]
if bn:
img = os.path.join(imgdir, f'feed_image_{self.image_counter}{os.path.splitext(bn)[-1]}')
try:
with open(img, 'wb') as fi, closing(self.browser.open(feed.image_url, timeout=self.timeout)) as r:
fi.write(r.read())
self.image_counter += 1 feed.image_url = img self.image_map[feed.image_url] = img except Exception:
pass if isinstance(feed.image_url, bytes):
feed.image_url = feed.image_url.decode(sys.getfilesystemencoding(), 'strict')

templ = (templates.TouchscreenFeedTemplate if self.touchscreen else templates.FeedTemplate)
templ = templ(lang=self.lang_for_html)
css = self.template_css + '\n\n' +(self.get_extra_css() or '')

return templ.generate(f, feeds, self.description_limiter, extra_css=css).render(doctype='xhtml')

def _fetch_article(self, url, dir_, f, a, num_of_feeds, preloaded=None):
br = self.browser if hasattr(self.get_browser, 'is_base_class_implementation'):
# We are using the default get_browser, which means no need to
# clone br = BasicNewsRecipe.get_browser(self)
else:
br = self.clone_browser(self.browser)
self.web2disk_options.browser = br fetcher = RecursiveFetcher(self.web2disk_options, self.log, self.image_map, self.css_map,
(url, f, a, num_of_feeds))
fetcher.browser = br fetcher.base_dir = dir_ fetcher.current_dir = dir_ fetcher.show_progress = False fetcher.image_url_processor = self.image_url_processor if preloaded is not None:
fetcher.preloaded_urls[url] = preloaded res, path, failures = fetcher.start_fetch(url), fetcher.downloaded_paths, fetcher.failed_links if not res or not os.path.exists(res):
msg = _('Could not fetch article.') + ' '
if self.debug:
msg += _('The debug traceback is available earlier in this log')
else:
msg += _('Run with -vv to see the reason')
raise Exception(msg)

return res, path, failures def fetch_article(self, url, dir, f, a, num_of_feeds):
return self._fetch_article(url, dir, f, a, num_of_feeds)

def fetch_obfuscated_article(self, url, dir, f, a, num_of_feeds):
x = self.get_obfuscated_article(url)
if isinstance(x, dict):
data = x['data']
if isinstance(data, str):
data = data.encode(self.encoding or 'utf-8')
url = x.get('url', url)
else:
with open(x, 'rb') as of:
data = of.read()
os.remove(x)
return self._fetch_article(url, dir, f, a, num_of_feeds, preloaded=data)

def fetch_embedded_article(self, article, dir, f, a, num_of_feeds):
templ = templates.EmbeddedContent()
raw = templ.generate(article).render('html')
with PersistentTemporaryFile('_feeds2disk.html') as pt:
pt.write(raw)
url = ('file:'+pt.name) if iswindows else ('file://'+pt.name)
return self._fetch_article(url, dir, f, a, num_of_feeds)

def remove_duplicate_articles(self, feeds):
seen_keys = defaultdict(set)
remove = []
for f in feeds:
for article in f:
for key in self.ignore_duplicate_articles:
val = getattr(article, key)
seen = seen_keys[key]
if val:
if val in seen:
remove.append((f, article))
else:
seen.add(val)

for feed, article in remove:
self.log.debug(f'Removing duplicate article: {article.title} from section: {feed.title}')
feed.remove_article(article)

if self.remove_empty_feeds:
feeds = [f for f in feeds if len(f) > 0]
return feeds def build_index(self):
self.report_progress(0, _('Fetching feeds...'))
feeds = None try:
feeds = feeds_from_index(self.parse_index(), oldest_article=self.oldest_article, max_articles_per_feed=self.max_articles_per_feed, log=self.log)
self.report_progress(0, _('Got feeds from index page'))
except NotImplementedError:
pass if feeds is None:
feeds = self.parse_feeds()

if not feeds:
raise ValueError('No articles found, aborting')

if self.ignore_duplicate_articles is not None:
feeds = self.remove_duplicate_articles(feeds)

self.report_progress(0, _('Trying to download cover...'))
self.download_cover()
self.report_progress(0, _('Generating masthead...'))
self.resolve_masthead()

if self.test:
feeds = feeds[:self.test[0]]
self.has_single_feed = len(feeds) == 1 index = os.path.join(self.output_dir, 'index.html')

html = self.feeds2index(feeds)
with open(index, 'wb') as fi:
fi.write(html)

self.jobs = []

if self.reverse_article_order:
for feed in feeds:
if hasattr(feed, 'reverse'):
feed.reverse()

self.feed_objects = feeds for f, feed in enumerate(feeds):
feed_dir = os.path.join(self.output_dir, f'feed_{f}')
if not os.path.isdir(feed_dir):
os.makedirs(feed_dir)

for a, article in enumerate(feed):
if a >= self.max_articles_per_feed:
break art_dir = os.path.join(feed_dir, f'article_{a}')
if not os.path.isdir(art_dir):
os.makedirs(art_dir)
try:
url = self.print_version(article.url)
except NotImplementedError:
url = article.url except Exception:
self.log.exception('Failed to find print version for: '+article.url)
url = None if not url:
continue func, arg = (self.fetch_embedded_article, article) \ if self.use_embedded_content or (self.use_embedded_content is None and feed.has_embedded_content()) \ else \ ((self.fetch_obfuscated_article if self.articles_are_obfuscated else self.fetch_article), url)
req = WorkRequest(func, (arg, art_dir, f, a, len(feed)),
{}, (f, a), self.article_downloaded, self.error_in_article_download)
req.feed = feed req.article = article req.feed_dir = feed_dir self.jobs.append(req)

self.jobs_done = 0 tp = ThreadPool(self.simultaneous_downloads)
for req in self.jobs:
tp.putRequest(req, block=True, timeout=0)

self.report_progress(0, ngettext(
'Starting download in a single thread...',
'Starting download [{} threads]...', self.simultaneous_downloads).format(self.simultaneous_downloads))
while True:
try:
tp.poll()
time.sleep(0.1)
except NoResultsPending:
break for f, feed in enumerate(feeds):
html = self.feed2index(f, feeds)
feed_dir = os.path.join(self.output_dir, f'feed_{f}')
with open(os.path.join(feed_dir, 'index.html'), 'wb') as fi:
fi.write(html)
self.create_opf(feeds)
self.report_progress(1, _('Feeds downloaded to %s')%index)

return index def _download_cover(self):
self.cover_path = None try:
cu = self.get_cover_url()
except Exception as err:
self.log.error(_('Could not download cover: %s')%as_unicode(err))
self.log.debug(traceback.format_exc())
else:
if not cu:
return cdata = None if hasattr(cu, 'read'):
cdata = cu.read()
cu = getattr(cu, 'name', 'cover.jpg')
elif os.access(cu, os.R_OK):
with open(cu, 'rb') as f:
cdata = f.read()
else:
self.report_progress(1, _('Downloading cover from %s')%cu)
with closing(self.browser.open(cu, timeout=self.timeout)) as r:
cdata = r.read()
if not cdata:
return ext = cu.split('/')[-1].rpartition('.')[-1].lower().strip()
if ext == 'pdf':
from calibre.ebooks.metadata.pdf import get_metadata stream = io.BytesIO(cdata)
cdata = None mi = get_metadata(stream)
if mi.cover_data and mi.cover_data[1]:
cdata = mi.cover_data[1]
if not cdata:
return if self.cover_margins[0] or self.cover_margins[1]:
cdata = image_to_data(add_borders_to_image(cdata, left=self.cover_margins[0],right=self.cover_margins[0], top=self.cover_margins[1],bottom=self.cover_margins[1], border_color=self.cover_margins[2]))

cpath = os.path.join(self.output_dir, 'cover.jpg')
save_cover_data_to(cdata, cpath)
self.cover_path = cpath def download_cover(self):
self.cover_path = None try:
self._download_cover()
except Exception:
self.log.exception('Failed to download cover')
self.cover_path = None def _download_masthead(self, mu):
if hasattr(mu, 'rpartition'):
ext = mu.rpartition('.')[-1]
if '?' in ext:
ext = ''
else:
ext = mu.name.rpartition('.')[-1]
ext = ext.lower() if ext else 'jpg'
mpath = os.path.join(self.output_dir, 'masthead_source.'+ext)
outfile = os.path.join(self.output_dir, 'mastheadImage.jpg')
if hasattr(mu, 'read'):
with open(mpath, 'wb') as mfile:
mfile.write(mu.read())
elif os.access(mu, os.R_OK):
with open(mpath, 'wb') as mfile:
mfile.write(open(mu, 'rb').read())
else:
with open(mpath, 'wb') as mfile, closing(self.browser.open(mu, timeout=self.timeout)) as r:
mfile.write(r.read())
self.report_progress(1, _('Masthead image downloaded'))
self.prepare_masthead_image(mpath, outfile)
self.masthead_path = outfile if os.path.exists(mpath):
os.remove(mpath)

def download_masthead(self, url):
try:
self._download_masthead(url)
except Exception:
self.log.exception('Failed to download supplied masthead_url')

def resolve_masthead(self):
self.masthead_path = None try:
murl = self.get_masthead_url()
except Exception:
self.log.exception('Failed to get masthead url')
murl = None if murl is not None:
# Try downloading the user-supplied masthead_url
# Failure sets self.masthead_path to None self.download_masthead(murl)
if self.masthead_path is None:
self.log.info('Synthesizing mastheadImage')
self.masthead_path = os.path.join(self.output_dir, 'mastheadImage.jpg')
try:
self.default_masthead_image(self.masthead_path)
except Exception:
self.log.exception('Failed to generate default masthead image')
self.masthead_path = None

[문서]
def default_cover(self, cover_file):
'''
Create a generic cover for recipes that don't have a cover
'''
try:
from calibre.ebooks.covers import create_cover title = self.title if isinstance(self.title, str) else \ self.title.decode(preferred_encoding, 'replace')
date = strftime(self.timefmt).replace('[', '').replace(']', '')
img_data = create_cover(title, [date])
cover_file.write(img_data)
cover_file.flush()
except Exception:
self.log.exception('Failed to generate default cover')
return False return True

[문서]
def get_masthead_title(self):
'Override in subclass to use something other than the recipe title'
return self.title

MI_WIDTH = 600
MI_HEIGHT = 60 def default_masthead_image(self, out_path):
from calibre.ebooks import generate_masthead generate_masthead(self.get_masthead_title(), output_path=out_path, width=self.MI_WIDTH, height=self.MI_HEIGHT)

def prepare_masthead_image(self, path_to_image, out_path):
prepare_masthead_image(path_to_image, out_path, self.MI_WIDTH, self.MI_HEIGHT)

[문서]
def publication_date(self):
'''
Use this method to set the date when this issue was published.
Defaults to the moment of download. Must return a :class:`datetime.datetime` object.
'''
return nowf()

def create_opf(self, feeds, dir=None):
if dir is None:
dir = self.output_dir title = self.short_title()
pdate = self.publication_date()
if self.output_profile.periodical_date_in_title:
title += strftime(self.timefmt, pdate)
mi = MetaInformation(title, [__appname__])
mi.publisher = __appname__ mi.author_sort = __appname__ if self.publication_type:
mi.publication_type = 'periodical:'+self.publication_type+':'+self.short_title()
mi.timestamp = nowf()
article_titles, aseen = [], set()
for (af, aa) in self.aborted_articles:
aseen.add(aa.title)
for (ff, fa, tb) in self.failed_downloads:
aseen.add(fa.title)
for f in feeds:
for a in f:
if a.title and a.title not in aseen:
aseen.add(a.title)
article_titles.append(force_unicode(a.title, 'utf-8'))

desc = self.description if not isinstance(desc, str):
desc = desc.decode('utf-8', 'replace')
mi.comments = (_('Articles in this issue:'
) + '\n\n' + '\n\n'.join(article_titles)) + '\n\n' + desc language = canonicalize_lang(self.language)
if language is not None:
mi.language = language mi.pubdate = pdate opf_path = os.path.join(dir, 'index.opf')
ncx_path = os.path.join(dir, 'index.ncx')

opf = OPFCreator(dir, mi)
# Add mastheadImage entry to <guide> section mp = getattr(self, 'masthead_path', None)
if mp is not None and os.access(mp, os.R_OK):
from calibre.ebooks.metadata.opf2 import Guide ref = Guide.Reference(os.path.basename(self.masthead_path), os.getcwd())
ref.type = 'masthead'
ref.title = 'Masthead Image'
opf.guide.append(ref)

manifest = [os.path.join(dir, f'feed_{i}') for i in range(len(feeds))]
manifest.append(os.path.join(dir, 'index.html'))
manifest.append(os.path.join(dir, 'index.ncx'))

# Get cover cpath = getattr(self, 'cover_path', None)
if cpath is None:
pf = open(os.path.join(dir, 'cover.jpg'), 'wb')
if self.default_cover(pf):
cpath = pf.name if cpath is not None and os.access(cpath, os.R_OK):
opf.cover = cpath manifest.append(cpath)

# Get masthead mpath = getattr(self, 'masthead_path', None)
if mpath is not None and os.access(mpath, os.R_OK):
manifest.append(mpath)

opf.create_manifest_from_files_in(manifest)
for mani in opf.manifest:
if mani.path.endswith('.ncx'):
mani.id = 'ncx'
if mani.path.endswith('mastheadImage.jpg'):
mani.id = 'masthead-image'

entries = ['index.html']
toc = TOC(base_path=dir)
self.play_order_counter = 0 self.play_order_map = {}

self.article_url_map = aumap = defaultdict(set)

def feed_index(num, parent):
f = feeds[num]
for j, a in enumerate(f):
if getattr(a, 'downloaded', False):
adir = f'feed_{num}/article_{j}/'
auth = a.author if not auth:
auth = None desc = a.text_summary if not desc:
desc = None else:
desc = self.description_limiter(desc)
tt = a.toc_thumbnail or None entries.append(f'{adir}index.html')
po = self.play_order_map.get(entries[-1], None)
if po is None:
self.play_order_counter += 1 po = self.play_order_counter arelpath = f'{adir}index.html'
for curl in self.canonicalize_internal_url(a.orig_url, is_link=False):
aumap[curl].add(arelpath)
article_toc_entry = parent.add_item(arelpath, None, a.title or _('Untitled article'), play_order=po, author=auth, description=desc, toc_thumbnail=tt)
for entry in a.internal_toc_entries:
anchor = entry.get('anchor')
if anchor:
self.play_order_counter += 1 po += 1 article_toc_entry.add_item( arelpath, entry['anchor'], entry['title'] or _('Unknown section'), play_order=po
)
last = os.path.join(self.output_dir, (f'{adir}index.html').replace('/', os.sep))
for sp in a.sub_pages:
prefix = os.path.commonprefix([opf_path, sp])
relp = sp[len(prefix):]
entries.append(relp.replace(os.sep, '/'))
last = sp if os.path.exists(last):
with open(last, 'rb') as fi:
src = fi.read().decode('utf-8')
soup = BeautifulSoup(src)
body = soup.find('body')
if body is not None:
prefix = '/'.join('..'for i in range(2*len(re.findall(r'link\d+', last))))
templ = self.navbar.generate(True, num, j, len(f), not self.has_single_feed, a.orig_url, __appname__, prefix=prefix, center=self.center_navbar)
elem = BeautifulSoup(templ.render(doctype='xhtml').decode('utf-8')).find('div')
body.insert(len(body.contents), elem)
with open(last, 'wb') as fi:
fi.write(str(soup).encode('utf-8'))
if len(feeds) == 0:
raise Exception('All feeds are empty, aborting.')

if len(feeds) > 1:
for i, f in enumerate(feeds):
entries.append(f'feed_{i}/index.html')
po = self.play_order_map.get(entries[-1], None)
if po is None:
self.play_order_counter += 1 po = self.play_order_counter auth = getattr(f, 'author', None)
if not auth:
auth = None desc = getattr(f, 'description', None)
if not desc:
desc = None feed_index(i, toc.add_item(f'feed_{i}/index.html', None, f.title, play_order=po, description=desc, author=auth))

else:
entries.append('feed_0/index.html')
feed_index(0, toc)

for i, p in enumerate(entries):
entries[i] = os.path.join(dir, p.replace('/', os.sep))
opf.create_spine(entries)
opf.set_toc(toc)

with open(opf_path, 'wb') as opf_file, open(ncx_path, 'wb') as ncx_file:
opf.render(opf_file, ncx_file)

def article_downloaded(self, request, result):
index = os.path.join(os.path.dirname(result[0]), 'index.html')
if index != result[0]:
if os.path.exists(index):
os.remove(index)
os.rename(result[0], index)
a = request.requestID[1]

article = request.article self.log.debug('Downloaded article:', article.title, 'from', article.url)
article.orig_url = article.url article.url = f'article_{a}/index.html'
article.downloaded = True article.sub_pages = result[1][1:]
self.jobs_done += 1 self.report_progress(float(self.jobs_done)/len(self.jobs),
_('Article downloaded: %s')%force_unicode(article.title))
if result[2]:
self.partial_failures.append((request.feed.title, article.title, article.url, result[2]))

def error_in_article_download(self, request, traceback):
self.jobs_done += 1 if traceback and re.search(r'^AbortArticle:', traceback, flags=re.M) is not None:
self.log.warn('Aborted download of article:', request.article.title,
'from', request.article.url)
self.report_progress(float(self.jobs_done)/len(self.jobs),
_('Article download aborted: %s')%force_unicode(request.article.title))
self.aborted_articles.append((request.feed, request.article))
else:
self.log.error('Failed to download article:', request.article.title,
'from', request.article.url)
self.log.debug(traceback)
self.log.debug('\n')
self.report_progress(float(self.jobs_done)/len(self.jobs),
_('Article download failed: %s')%force_unicode(request.article.title))
self.failed_downloads.append((request.feed, request.article, traceback))

[문서]
def parse_feeds(self):
'''
Create a list of articles from the list of feeds returned by :meth:`BasicNewsRecipe.get_feeds`.
Return a list of :class:`Feed` objects.
'''
feeds = self.get_feeds()
parsed_feeds = []
br = self.browser for obj in feeds:
if isinstance(obj, (str, bytes)):
title, url = None, obj else:
title, url = obj if isinstance(title, bytes):
title = title.decode('utf-8')
if isinstance(url, bytes):
url = url.decode('utf-8')
if url.startswith('feed://'):
url = 'http'+url[4:]
self.report_progress(0, _('Fetching feed')+f' {title or url}...')
try:
purl = urlparse(url, allow_fragments=False)
if purl.username or purl.password:
hostname = purl.hostname if purl.port:
hostname += f':{purl.port}'
url = purl._replace(netloc=hostname).geturl()
if purl.username and purl.password:
br.add_password(url, purl.username, purl.password)
with closing(br.open_novisit(url, timeout=self.timeout)) as f:
raw = f.read()
parsed_feeds.append(feed_from_xml( raw, title=title, log=self.log, oldest_article=self.oldest_article, max_articles_per_feed=self.max_articles_per_feed, get_article_url=self.get_article_url
))
except Exception as err:
feed = Feed()
msg = f'Failed feed: {title or url}'
feed.populate_from_preparsed_feed(msg, [])
feed.description = as_unicode(err)
parsed_feeds.append(feed)
self.log.exception(msg)
delay = self.get_url_specific_delay(url)
if delay > 0:
time.sleep(delay)

remove = [fl for fl in parsed_feeds if len(fl) == 0 and self.remove_empty_feeds]
for f in remove:
parsed_feeds.remove(f)

return parsed_feeds[문서]
@classmethod def tag_to_string(self, tag, use_alt=True, normalize_whitespace=True):
'''
Convenience method to take a
`BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/>`_
:code:`Tag` and extract the text from it recursively, including any CDATA sections and alt tag attributes. Return a possibly empty Unicode string.

`use_alt`: If `True` try to use the alt attribute for tags that don't have any textual content

`tag`: `BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/>`_
:code:`Tag`
'''
if tag is None:
return ''
if isinstance(tag, (str, bytes)):
return tag if callable(getattr(tag, 'xpath', None)) and not hasattr(tag, 'contents'): # a lxml tag from lxml.etree import tostring ans = tostring(tag, method='text', encoding='unicode', with_tail=False)
else:
strings = []
for item in tag.contents:
if isinstance(item, (NavigableString, CData)):
strings.append(item.string)
elif isinstance(item, Tag):
res = self.tag_to_string(item)
if res:
strings.append(res)
elif use_alt:
try:
strings.append(item['alt'])
except KeyError:
pass ans = ''.join(strings)
if normalize_whitespace:
ans = re.sub(r'\s+', ' ', ans)
return ans@classmethod def soup(cls, raw):
return BeautifulSoup(raw)

[문서]
@classmethod def adeify_images(cls, soup):
'''
If your recipe when converted to EPUB has problems with images when viewed in Adobe Digital Editions, call this method from within
:meth:`postprocess_html`.
'''
for item in soup.findAll('img'):
for attrib in ['height','width','border','align','style']:
try:
del item[attrib]
except KeyError:
pass oldParent = item.parent myIndex = oldParent.contents.index(item)
item.extract()
divtag = soup.new_tag('div')
brtag = soup.new_tag('br')
oldParent.insert(myIndex, divtag)
divtag.append(item)
divtag.append(brtag)
return soup
def internal_postprocess_book(self, oeb, opts, log):
seen = set()
for i, item in enumerate(oeb.spine):
for a in item.data.xpath('//*[local-name()="a" and @href]'):
if (rel := a.get('rel')) == 'calibre-downloaded-from':
continue url = a.get('href')
if not url:
continue if rel in ('articlenextlink', 'articleprevlink'):
abshref = item.abshref(url)
if abshref not in oeb.manifest.hrefs:
if rel == 'articlenextlink':
nextitem = oeb.spine[i + 1] if i + 1 < len(oeb.spine) else None else:
nextitem = oeb.spine[i - 1] if i else None if nextitem is None:
a.text = None a.attrib.pop('href')
else:
a.set('href', item.relhref(nextitem.href))
continue if self.resolve_internal_links and self.article_url_map:
for curl in self.canonicalize_internal_url(url):
articles = self.article_url_map.get(curl)
if articles:
arelpath = sorted(articles, key=numeric_sort_key)[0]
a.set('href', item.relhref(arelpath))
if a.text and len(a) == 0:
a.text = a.text + '·' # mark as local link if url not in seen:
log.debug(f'Resolved internal URL: {url} -> {arelpath}')
seen.add(url)class CustomIndexRecipe(BasicNewsRecipe):

def custom_index(self):
'''
Return the filesystem path to a custom HTML document that will serve as the index for this recipe. The index document will typically contain many `<a href="...">` tags that point to resources on the internet that should be downloaded.
'''
raise NotImplementedError def create_opf(self):
mi = MetaInformation(self.title + strftime(self.timefmt), [__appname__])
mi.publisher = __appname__ mi.author_sort = __appname__ mi = OPFCreator(self.output_dir, mi)
mi.create_manifest_from_files_in([self.output_dir])
mi.create_spine([os.path.join(self.output_dir, 'index.html')])
with open(os.path.join(self.output_dir, 'index.opf'), 'wb') as opf_file:
mi.render(opf_file)

def download(self):
index = os.path.abspath(self.custom_index())
url = 'file:'+index if iswindows else 'file://'+index self.web2disk_options.browser = self.clone_browser(self.browser)
fetcher = RecursiveFetcher(self.web2disk_options, self.log)
fetcher.base_dir = self.output_dir fetcher.current_dir = self.output_dir fetcher.show_progress = False res = fetcher.start_fetch(url)
self.create_opf()
return res class AutomaticNewsRecipe(BasicNewsRecipe):

auto_cleanup = True class CalibrePeriodical(BasicNewsRecipe):

#: Set this to the slug for the calibre periodical calibre_periodicals_slug = None

LOG_IN = 'https://news.calibre-ebook.com/accounts/login'
needs_subscription = True
__author__ = 'calibre Periodicals'

def get_browser(self):
br = BasicNewsRecipe.get_browser(self)
br.open(self.LOG_IN)
br.select_form(name='login')
br['username'] = self.username br['password'] = self.password raw = br.submit().read()
if 'href="/my-account"' not in raw:
raise LoginFailed(
_('Failed to log in, check your username and password for'
' the calibre Periodicals service.'))

return br get_browser.is_base_class_implementation = True def download(self):
self.log('Fetching downloaded recipe')
try:
raw = self.browser.open_novisit( f'https://news.calibre-ebook.com/subscribed_files/{self.calibre_periodicals_slug}/0/temp.downloaded_recipe'
).read()
except Exception as e:
if hasattr(e, 'getcode') and e.getcode() == 403:
raise DownloadDenied(
_('You do not have permission to download this issue.'
' Either your subscription has expired or you have'
' exceeded the maximum allowed downloads for today.'))
raise f = io.BytesIO(raw)
from calibre.utils.zipfile import ZipFile zf = ZipFile(f)
zf.extractall()
zf.close()
from glob import glob from calibre.web.feeds.recipes import compile_recipe try:
recipe = compile_recipe(open(glob('*.recipe')[0],
'rb').read())
self.conversion_options = recipe.conversion_options except Exception:
self.log.exception('Failed to compile downloaded recipe')
return os.path.abspath('index.html')

© 저작권 Kovid Goyal.
마지막 업데이트 날짜: 4월 10, 2026.

Table of Contents

캘리버(Calibre) 사용자 설명서

그래픽 사용자 인터페이스

1. 동작

1. 책 추가하기
2. 책정보 편집하기
3. 책 변환하기
4. 보기
5. 장치로 보내기
6. 뉴스 가져오기
7. 라이브러리
8. 장치
9. 디스크에 저장하기
10. 연결/공유
11. 책 지우기

2. 환경설정
3. 범주
4. 검색 및 정렬
5. 검색 인터페이스
6. 검색 저장
7. 모든 책의 전체 텍스트 검색
8. 가상 라이브러리
9. 책 임시 표시
10. 파일 이름에서 메타데이터 추측중
11. 책 상세정보
12. 태그 탐색기
13. 표지 격자 보기
14. 책장 보기
15. 표지 탐색기
16. 저자, 시리즈 등에 메모 추가하기
17. 빨리보기
18. 작업
19. 키보드 단축키

즐겨찾는 뉴스 웹사이트 추가

1. 완전 자동 가져오기

1. calibre 블로그

2. bbc.co.uk

2. 가져오기 프로세스 맞춤 설정

1. bbc.co.uk 인쇄 버전 사용하기
2. 기사 스타일 대체
3. 자르고 다듬기
4. 실제 예시

3. 새 레시피 개발 팁

4. 더 읽을 거리

5. API 문서

1. 레시피 API 문서

1. BasicNewsRecipe

전자책 뷰어

1. 전자책 뷰어 시작하기

2. 전자책 탐색하기

1. 책갈피
2. 목차
3. 위치로 이동하기
4. 참조 모드

3. 강조표시 텍스트
4. 소리내어 읽기
5. 텍스트 검색
6. 키보드만으로 링크 따라가기
7. 사용자의 독서 체험의 모양새 사용자 정의
8. 사전 조회
9. 텍스트 및 이미지 복사중
10. 이미지 확대
11. 현재 책의 종이책 판본과 동기화
12. 키보드 단축키
13. 재흐름이 불가능한 콘텐츠

14. calibre 전자책 뷰어에서 잘 작동하도록 책 설계하기

전자책 변환

1. 소개

2. 모양새

1. 글꼴
2. 텍스트
3. 배치
4. 스타일
5. 스타일 변형
6. HTML 변환

3. 페이지 설정
4. 합리적 추측에 의한(Heuristic) 처리
5. 검색 & 교체

6. 구조 탐지

1. 장과 페이지 나눔

2. 기타

7. 목차

8. HTML 입력 문서를 변환할 때 이미지를 장 제목으로 사용하기

9. 목차 항목의 텍스트를 태그 속성으로 제공하기

10. 변환 옵션의 설정/저장 방식

11. 형식별 팁

1. Microsoft Word 문서 변환

1. 오래된 .doc 파일

2. TXT 문서 변환
3. PDF 문서 변환
4. 만화책 묶음
5. EPUB 고급 서식 데모
6. ODT 문서 변환

7. PDF로 변환하기

1. 머리글과 바닥글

2. 인쇄용 목차

3. 개별 HTML 파일에 대한 사용자 지정 페이지 여백

전자책 편집하는 중
기본 작업흐름

파일 탐색기

1. 파일 이름 바꾸기
2. 파일 병합
3. 텍스트 파일 순서 변경
4. 표지 지정
5. 파일 삭제
6. 파일 내보내기

7. 새 이미지/글꼴/기타 추가 또는 새 빈 파일 만들기

8. 파일 바꾸기

9. HTML 파일에 스타일시트 효율적으로 연결하기

검색 & 교체

1. 저장된 검색
2. 함수 모드
3. HTML 태그를 무시하고 검색

자동화 도구

1. 목차 편집
2. 책 검사
3. 표지 추가
4. 참조된 글꼴 포함하기

5. 내장된 글꼴의 사용된 문자만 취하기 (Subsetting embedded fonts)

6. 지능적 문장부호 처리
7. CSS 속성 변형하는 중
8. 사용하지 않는 CSS 규칙 제거 중
9. HTML 수정
10. 파일 서식 정리
11. 본문 안에 목차 삽입
12. 의미 정보 설정
13. 스타일 정보 필터링
14. 책 내부 구조 업그레이드

체크포인트

실시간 미리보기 패널

1. HTML 파일 분할

실시간 CSS 패널

기타 도구

1. 목차 보기

2. 책에 있는 단어의 철자 검사

1. 새 사전 추가

3. 특수 문자 삽입
4. 코드 검사기 보기
5. 외부 링크 검사
6. 외부 리소스 다운로드 중
7. 파일을 형식별 폴더로 정리

8. 다른 전자책 형식의 파일을 EPUB로 가져오는 중

9. 보고서 도구

코드 편집기의 특수 기능

1. 구문 강조표시
2. 문맥에 맞는 도움말
3. 자동 완성
4. 토막글

calibre 콘텐츠 서버

1. 다른 장치에서 콘텐츠 서버에 접근하기

1. 홈 네트워크의 장치에서 서버에 접근하기

1. 홈 네트워크 연결 문제 해결

2. 인터넷 어디서든 서버에 접근하기

2. 서버 인터페이스

1. 책 목록

2. 책 뷰어

3. 브라우저 지원
4. 오프라인 지원 활성화
5. 명령줄에서만 사용자 계정 관리

6. calibre 콘텐츠 서버를 다른 서버에 통합하기

1. 전체 가상 호스트 사용

2. URL 접두사 사용

7. 최신 Linux 시스템에서 calibre 서버용 서비스 만들기

전자책 비교

1. 비교 보기 이해하기

2. 비교 도구 실행하기

1. 두 전자책 파일 비교하기

2. ORIGINAL_FMT와 FMT 비교하기

3. 편집 중 체크포인트와 책의 현재 상태 비교하기

전자책 메타데이터 편집

1. 한 번에 한 권의 메타데이터 편집

1. 메타데이터 다운로드
2. 책 형식 관리
3. 표지에 대한 모든 것

2. 한 번에 여러 책의 메타데이터 편집

1. 찾아 바꾸기

2. 메타데이터 일괄 다운로드

3. 책에 추가 데이터 파일 추가

자주 묻는 질문(FAQ)

전자책 형식 변환

1. 캘리버는 어떤 형식으로/에서 변환을 지원합니까?

2. 변환에 가장 적합한 원본 형식은 무엇인가요?

3. PDF 파일을 변환했는데 결과물에 여러 문제가 있습니다?

4. 영문이 아닌 문자가 들어 있거나 스마트 인용부호가 포함된 파일은 어떻게 변환하나요?

5. MOBI 파일의 목차는 왜 그런가요?

6. 특정 순서로 된 HTML 파일 모음을 어떻게 변환하나요?

7. calibre로 만든 EPUB이 유효하지 않습니다?

8. 변환 도구의 고급 기능 일부는 어떻게 사용하나요?

장치 통합

1. calibre는 어떤 장치를 지원하나요?

2. 내 장치가 calibre에서 지원되도록 하려면 어떻게 해야 하나요?

3. 내 장치가 calibre에서 감지되지 않습니다?

4. 내 장치가 표준이 아니거나 특이합니다. 연결하려면 어떻게 하나요?

5. iPad/iPhone/iPod touch에서 calibre를 사용하려면 어떻게 하나요?

1. 컨텐츠 서버 사용

6. Android 휴대폰/태블릿이나 Kindle Fire에서 calibre를 사용하려면 어떻게 하나요?

1. USB 케이블 사용

2. 무선 연결

7. Kindle이나 다른 읽기 장치의 웹 브라우저에서 calibre 책에 접근할 수 있나요?

8. calibre로 이메일을 보낼 수 없습니다?

9. 내 장치가 Linux에 읽기 전용으로 마운트되고 있으므로 캘리버가 장치에 연결할 수 없습니까?

10. 왜 calibre는 Kindle의 컬렉션이나 Nook의 선반을 지원하지 않나요?

11. 내 Kobo Touch/Glo/등에서 캘리버를 사용하려고 하면 오류가 발생합니까?

12. 내가 e-ink Kindle로 보낸 책의 표지가 잠깐 나타났다가 일반 표지로 바뀝니다?

13. Kindle ColorSoft 이상으로 보낸 책의 표지가 책 목록에 나타나지 않습니다?

14. 내 MOBI 파일 표지가 Kindle for PC/Kindle for Android/iPad 등에서 더 이상 보이지 않습니다.

15. calibre로 Kindle에 책 몇 권을 전송했는데 나타나지 않습니다?

라이브러리 관리

1. 책 파일은 어디에 저장되나요?

2. 캘리버는 저자 이름과 정렬을 어떻게 관리합니까?

3. 왜 calibre는 내가 원하는 폴더 구조에 책을 저장하지 못하게 하나요?

4. 왜 calibre에는 foo용 열이 없나요?

5. 형식이나 ISBN을 표시하는 열을 만들 수 있나요?

6. calibre 데이터를 한 컴퓨터에서 다른 컴퓨터로 옮기려면 어떻게 하나요?

7. calibre의 책 목록이 비어 있습니다!

8. 네트워크 드라이브/NAS의 내 캘리버 라이브러리에 오류가 발생합니까?

기타

1. Amazon이 MOBI 파일 이메일 전송을 중단하나요?

2. 내가 좋아하는 뉴스 사이트에서 calibre가 뉴스를 내려받게 하고 싶습니다.

3. 이름을 왜 calibre라고 지었나요?

4. macOS에서는 왜 일부 글꼴만 calibre에 표시되나요?

5. Windows에서 calibre가 시작되지 않습니다?

6. calibre가 가끔 멈추거나 충돌합니다?

7. 뷰어를 사용하거나 변환을 하면 Windows에서 permission denied 오류가 납니다.

8. macOS에서 calibre가 시작되지 않거나 충돌합니다?

9. calibre 전자책 뷰어를 실행하면 검은 화면 또는 흰 화면만 나옵니다?

10. 설치 프로그램을 내려받았는데 동작하지 않습니다?

11. 내 백신 프로그램이 calibre를 바이러스/트로이 목마라고 주장합니다?

12. calibre는 어떻게 백업하나요?

13. 구입한 EPUB 책을 calibre와 함께 사용하려면 어떻게 하나요(.acsm 파일은 어떻게 하나요)?

14. “권한 거부됨” 오류가 발생합니까?

15. 댓글 메타데이터를 리더기에서도 보이게 할 수 있나요?

16. calibre가 내 HTTP 프록시를 사용하게 하려면 어떻게 하나요?

17. calibre에 어떤 기능을 추가하고 싶습니다. 어떻게 해야 하나요?

18. 왜 calibre에는 자동 업데이트가 없나요?

19. calibre는 어떤 라이선스로 배포되나요?

20. USB 메모리에서 calibre를 실행하려면 어떻게 하나요?

21. 내 Linux 서버에서 뉴스 다운로드나 콘텐츠 서버 같은 calibre 기능 일부를 실행하려면 어떻게 하나요?

튜토리얼
즐겨찾는 뉴스 웹사이트 추가

1. 완전 자동 가져오기

1. calibre 블로그

2. bbc.co.uk

2. 가져오기 프로세스 맞춤 설정

1. bbc.co.uk 인쇄 버전 사용하기
2. 기사 스타일 대체
3. 자르고 다듬기
4. 실제 예시

3. 새 레시피 개발 팁

4. 더 읽을 거리

5. API 문서

1. 레시피 API 문서

책의 하위 그룹 관리, 예를 들어 “장르”

1. 설정
2. 검색
3. 제한
4. 유용한 템플릿 함수

XPath 튜토리얼

1. 태그 이름으로 선택
2. 속성으로 선택
3. 태그 내용으로 선택
4. 샘플 전자책
5. XPath 내장 함수

calibre 템플릿 언어

1. 기본 템플릿
2. 고급 서식 지정
3. 템플릿을 사용해 사용자 정의 열 정의하기
4. 템플릿과 플러그보드

5. 템플릿에서 함수 사용하기 - Single Function Mode

6. General Program Mode

7. 템플릿 식에서 더 복잡한 프로그램 사용하기 - Template Program Mode

8. Python Template Mode
9. 템플릿과 URL
10. 저장된 템플릿
11. 템플릿에 추가 정보 제공하기
12. 모드 간 차이점에 대한 참고
13. 사용자 정의 Python 템플릿 함수

14. 서로 다른 문맥에서 템플릿을 사용할 때의 특별 참고사항

15. 저장/전송 템플릿에 대한 특별 참고사항

16. 팁

17. 템플릿 기능 참조

1. Reference for all built-in template language functions

calibre에서 정규 표현식 사용하기

1. 먼저, 경고와 용기의 말씀

2. calibre 어디에서 정규 표현식을 사용할 수 있나요?

3. 정규 표현식이 도대체 무엇인가요?

4. 설명해 주시겠어요?

5. 그리 나쁘지 않네요. 다음은요?

6. 오, 깔끔하네요! 이해가 되기 시작합니다!

7. 이 특수 문자들이 아주 깔끔하지만, 점이나 물음표와 일치시키고 싶으면 어떻게 하나요?

8. 그럼 가장 유용한 집합은 무엇인가요?

9. 하지만 일치시키고 싶은 몇 가지 변형된 문자열이 있으면 복잡해지나요?

10. 놓친 것이 있는데…

11. 처음에 정규 표현식의 대소문자 구분을 없애는 방법이 있다고 하셨죠?

12. 이제 정규 표현식을 이해하기 시작한 것 같은데… calibre에서 어떻게 사용하나요?

1. 변환
2. 책 추가
3. 메타데이터 일괄 편집

13. 빠른 참조

1. 정규 표현식 구문 빠른 참조

14. 크레딧

직접 플러그인을 작성하여 calibre 기능 확장하기

1. calibre 플러그인의 구조

2. 사용자 인터페이스 플러그인

1. __init__.py
2. ui.py
3. main.py
4. 플러그인 ZIP 파일에서 리소스 가져오기
5. 플러그인의 사용자 구성 활성화

3. 책 편집 플러그인

1. main.py

4. 플러그인에 번역 추가
5. 플러그인 API
6. 플러그인 디버깅
7. 더 많은 플러그인 예시
8. 다른 사람과 플러그인 공유

전자책의 수학 조판하기

1. 수학이 포함된 간단한 HTML 파일

2. 자세한 정보

AZW3 • EPUB • MOBI 카탈로그 만들기

1. 카탈로그에 포함할 책 선택
2. 포함된 섹션
3. 접두사
4. 제외된 책
5. 제외된 장르
6. 기타 옵션
7. 사용자 정의 카탈로그 표지
8. 추가 도움말 자료

가상 라이브러리

1. 가상 라이브러리 만들기

1. 유용한 가상 라이브러리 예시

2. 가상 라이브러리 작업
3. 검색에서 가상 라이브러리 사용하기
4. 추가 제한 사용하기

calibre:// URL 스키마

1. 특정 라이브러리로 전환
2. 캘리버에서 특정 책 표시
3. 전자책 뷰어에서 특정 위치의 특정 책 열기
4. 책 검색

5. 특정 라이브러리의 책에 대한 책 세부정보 창 열기

6. 저자/시리즈 등과 연결된 메모 열기

7. URL 매개변수의 16진수 인코딩

캘리버 사용자 정의

1. 환경 변수
2. 트윅
3. 아이콘, 템플릿 등 재정의
4. 캘리버용 사용자 정의 아이콘 테마 만들기
5. 플러그인으로 캘리버 사용자 정의

명령행 인터페이스
문서화된 명령어
calibre

1. [옵션]

calibre-customize

1. [옵션]

calibre-debug

1. [옵션]

calibre-server

1. [옵션]

calibre-smtp

1. [옵션]

calibredb

1. 전역 옵션
2. list
3. add
4. remove
5. add_format
6. remove_format
7. show_metadata
8. set_metadata
9. export
10. catalog
11. saved_searches
12. add_custom_column
13. custom_columns
14. remove_custom_column
15. set_custom
16. restore_database
17. check_library
18. list_categories
19. backup_metadata
20. clone
21. embed_metadata
22. search
23. fts_index
24. fts_search

ebook-convert

1. 입력 옵션
2. 출력 옵션
3. 모양새
4. 합리적 추측에 의한(Heuristic) 처리
5. 검색 및 교체
6. 구조 탐지
7. 목차
8. 메타데이터
9. 디버그

ebook-edit

1. [옵션]

ebook-meta

1. [옵션]

ebook-polish

1. [옵션]

ebook-viewer

1. [옵션]

fetch-ebook-metadata

1. [옵션]

lrf2lrs

1. [옵션]

lrfviewer

1. [옵션]

lrs2lrf

1. [옵션]

web2disk

1. [옵션]

비문서화된 명령어

캘리버 개발 환경 설정

1. 설계 철학

1. 코드 구성

2. 코드 가져오기

1. 변경 사항 제출하여 포함시키기

3. Windows 개발 환경
4. macOS 개발 환경
5. Linux 개발 환경

6. 동일한 컴퓨터에 별도의 “일반” 및 “개발” 캘리버 설치 갖기

7. 디버깅 팁

1. print 문 사용하기
2. 대화형 Python 인터프리터 사용하기
3. Python 디버거를 원격 디버거로 사용하기

4. 즐겨 사용하는 Python IDE에서 디버거 사용하기

5. 캘리버 Python 환경에서 임의 스크립트 실행하기

8. 프로젝트에서 캘리버 사용하기

1. 캘리버 바이너리 설치

2. Linux에서 소스 설치

9. 캘리버 다양한 부분의 API 문서

1. 레시피 API 문서

1. BasicNewsRecipe

2. 플러그인용 API 문서

1. Plugin
2. FileTypePlugin
3. 메타데이터 플러그인
4. 카탈로그 플러그인
5. 메타데이터 다운로드 플러그인
6. 변환 플러그인
7. 장치 드라이버
8. 사용자 인터페이스 동작
9. 환경설정 플러그인

3. 데이터베이스 인터페이스에 대한 API 문서

1. Cache

4. 전자책 편집 도구용 API 문서

1. Container 객체
2. 컨테이너 안의 구성 파일 관리
3. 예쁘게 정리하기 및 파싱 오류 자동 수정
4. 책 재킷 관리
5. 파일 분할 및 병합
6. 표지 관리
7. CSS 다루기
8. 목차 다루기
9. 책 편집 도구
10. 편집기 사용자 인터페이스 제어

디지털 저작권 관리 (DRM)

1. DRM은 개인적으로 무엇을 의미하나요?

2. DRM은 저자에게 무엇을 해주나요?

3. DRM과 자유

4. 캘리버는 왜 DRM을 지원하지 않나요?

5. 콘텐츠 제공자에 대한 캘리버의 견해는 무엇인가요?

6. DRM에 맞서 싸우려면 어떻게 해야 하나요?

용어집